Skip to content

Commit

Permalink
feat(payments): body payments + testing
Browse files Browse the repository at this point in the history
  • Loading branch information
serge1peshcoff committed Apr 4, 2020
1 parent d1796fc commit 5d14e76
Show file tree
Hide file tree
Showing 17 changed files with 1,054 additions and 9 deletions.
27 changes: 25 additions & 2 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
module.exports = {
FIELDS_TO_UPDATE: {
USER: {
CREATE: ['username', 'email', 'password', 'first_name', 'last_name', 'date_of_birth', 'gender', 'phone', 'address', 'about_me'],
UPDATE: ['username', 'first_name', 'last_name', 'date_of_birth', 'gender', 'phone', 'address', 'about_me']
CREATE: [
'username',
'email',
'password',
'first_name',
'last_name',
'date_of_birth',
'gender',
'phone',
'address',
'about_me'
],
UPDATE: [
'username',
'first_name',
'last_name',
'date_of_birth',
'gender',
'phone',
'address',
'about_me'
]
},
CIRCLE: {
CREATE: ['name', 'description']
},
PAYMENT: {
UPDATE: ['starts', 'expires', 'amount', 'currency', 'comment', 'invoice_name', 'invoice_address']
}
},
FIELDS_TO_QUERY: {
BODY: ['code', 'name'],
Expand Down
10 changes: 10 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const permissions = require('../middlewares/permissions');
const memberships = require('../middlewares/memberships');
const joinRequests = require('../middlewares/join-requests');
const bodyCampaigns = require('../middlewares/body-campaigns');
const payments = require('../middlewares/payments');

const GeneralRouter = router({ mergeParams: true });
const MemberRouter = router({ mergeParams: true });
Expand All @@ -34,6 +35,7 @@ const CirclesRouter = router({ mergeParams: true });
const PermissionsRouter = router({ mergeParams: true });
const CampaignsRouter = router({ mergeParams: true });
const BodyCampaignsRouter = router({ mergeParams: true });
const PaymentsRouter = router({ mergeParams: true });

const server = express();
server.use(bodyParser.json());
Expand Down Expand Up @@ -98,6 +100,8 @@ BodiesRouter.get('/members', memberships.listAllMemberships);
BodiesRouter.post('/create-member', bodies.createMember);
BodiesRouter.get('/join-requests', joinRequests.listAllJoinRequests);
BodiesRouter.post('/join-requests', joinRequests.createJoinRequest);
BodiesRouter.get('/payments', payments.listAllPayments);
BodiesRouter.post('/payments', payments.createPayment);
BodiesRouter.put('/status', bodies.setBodyStatus);
BodiesRouter.put('/', bodies.updateBody);

Expand Down Expand Up @@ -137,10 +141,16 @@ BodyCampaignsRouter.get('/', bodyCampaigns.getCampaign);
BodyCampaignsRouter.put('/', bodyCampaigns.updateCampaign);
BodyCampaignsRouter.delete('/', bodyCampaigns.deleteCampaign);

// Everything related to a specific payment. Auth only.
PaymentsRouter.use(middlewares.maybeAuthorize, middlewares.ensureAuthorized, fetch.fetchBody, fetch.fetchPayment);
PaymentsRouter.put('/', payments.updatePayment);
PaymentsRouter.delete('/', payments.deletePayment);

server.use('/members/:user_id', MemberRouter);
server.use('/bodies/:body_id/members/:membership_id', MembershipsRouter);
server.use('/bodies/:body_id/join-requests/:request_id', JoinRequestsRouter);
server.use('/bodies/:body_id/campaigns/:campaign_id', BodyCampaignsRouter);
server.use('/bodies/:body_id/payments/:payment_id', PaymentsRouter);
server.use('/bodies/:body_id', BodiesRouter);
server.use('/circles/:circle_id', CirclesRouter);
server.use('/permissions/:permission_id', PermissionsRouter);
Expand Down
3 changes: 2 additions & 1 deletion middlewares/bodies.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { Body, User, BodyMembership, JoinRequest, Circle } = require('../models');
const { Body, User, BodyMembership, JoinRequest, Circle, Payment } = require('../models');
const helpers = require('../lib/helpers');
const constants = require('../lib/constants');
const errors = require('../lib/errors');
Expand Down Expand Up @@ -70,6 +70,7 @@ exports.setBodyStatus = async (req, res) => {
await JoinRequest.destroy({ where: { body_id: req.currentBody.id } }, { transaction: t });
await BodyMembership.destroy({ where: { body_id: req.currentBody.id } }, { transaction: t });
await Circle.destroy({ where: { body_id: req.currentBody.id } }, { transaction: t });
await Payment.destroy({ where: { body_id: req.currentBody.id } }, { transaction: t });
});
return res.json({
success: true,
Expand Down
24 changes: 23 additions & 1 deletion middlewares/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const {
Permission,
Campaign,
BodyMembership,
JoinRequest
JoinRequest,
Payment
} = require('../models');

const { Sequelize } = require('../lib/sequelize');
Expand Down Expand Up @@ -188,3 +189,24 @@ exports.fetchBodyCampaign = async (req, res, next) => {
req.currentBodyCampaign = campaign;
return next();
};

exports.fetchPayment = async (req, res, next) => {
// searching the payment by id if it's numeric
if (!helpers.isNumber(req.params.payment_id)) {
return errors.makeBadRequestError(res, 'Payment ID is invalid.');
}

const payment = await Payment.findOne({
where: {
id: Number(req.params.payment_id),
body_id: req.currentBody.id
}
});

if (!payment) {
return errors.makeNotFoundError(res, 'Payment is not found.');
}

req.currentPayment = payment;
return next();
};
76 changes: 76 additions & 0 deletions middlewares/payments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const { Payment, User, BodyMembership } = require('../models');
const errors = require('../lib/errors');
const helpers = require('../lib/helpers');
const constants = require('../lib/constants');

exports.listAllPayments = async (req, res) => {
if (!req.permissions.hasPermission('view:payment')) {
return errors.makeForbiddenError(res, 'Permission view:payment is required, but not present.');
}

const result = await Payment.findAndCountAll({
where: { body_id: req.currentBody.id },
...helpers.getPagination(req.query),
order: helpers.getSorting(req.query)
});

return res.json({
success: true,
data: result.rows,
meta: { count: result.count }
});
};

exports.createPayment = async (req, res) => {
if (!req.permissions.hasPermission('create:payment')) {
return errors.makeForbiddenError(res, 'Permission create:payment is required, but not present.');
}

const user = await User.findOne({
where: { id: req.body.user_id },
include: [BodyMembership]
});

if (!user) {
return errors.makeNotFoundError(res, 'User is not found.');
}

if (!user.body_memberships.some((membership) => membership.body_id === req.currentBody.id)) {
return errors.makeForbiddenError(res, 'User is not a member of a body.');
}

const payment = await Payment.create({
...req.body,
user_id: user.id,
body_id: req.currentBody.id
});

return res.json({
success: true,
data: payment
});
};

exports.updatePayment = async (req, res) => {
if (!req.permissions.hasPermission('update:payment')) {
return errors.makeForbiddenError(res, 'Permission update:payment is required, but not present.');
}

await req.currentPayment.update(req.body, { fields: constants.FIELDS_TO_UPDATE.PAYMENT.UPDATE });
return res.json({
success: true,
data: req.currentPayment
});
};

exports.deletePayment = async (req, res) => {
if (!req.permissions.hasPermission('delete:payment')) {
return errors.makeForbiddenError(res, 'Permission delete:payment is required, but not present.');
}

await req.currentPayment.destroy();
return res.json({
success: true,
message: 'Payment is deleted.'
});
};
65 changes: 65 additions & 0 deletions migrations/20200401193849-create-payments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('payments', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
},
onDelete: 'CASCADE'
},
body_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'bodies',
key: 'id'
},
onDelete: 'CASCADE'
},
starts: {
type: Sequelize.DATEONLY,
allowNull: false,
},
expires: {
type: Sequelize.DATEONLY,
allowNull: false,
},
amount: {
type: Sequelize.INTEGER,
allowNull: false,
},
currency: {
type: Sequelize.STRING,
allowNull: false,
},
comment: {
type: Sequelize.TEXT,
allowNull: true,
},
invoice_name: {
type: Sequelize.TEXT,
allowNull: true,
},
invoice_address: {
type: Sequelize.TEXT,
allowNull: true,
},
created_at: {
allowNull: false,
type: Sequelize.DATE
},
updated_at: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: (queryInterface) => queryInterface.dropTable('payments')
};
2 changes: 1 addition & 1 deletion models/JoinRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const JoinRequest = sequelize.define('join_request', {
validate: {
isIn: {
args: [['pending', 'accepted']],
msg: 'Satus should be one of these: "pending", "accepted".'
msg: 'Status should be one of these: "pending", "accepted".'
}
}
},
Expand Down
73 changes: 73 additions & 0 deletions models/Payment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const moment = require('moment');

const { Sequelize, sequelize } = require('../lib/sequelize');

const Payment = sequelize.define('payment', {
starts: {
type: Sequelize.DATEONLY,
allowNull: false,
defaultValue: '',
validate: {
notEmpty: { msg: 'Starts date should be set.' },
isDate: { msg: 'Starts date should be valid.' }
}
},
expires: {
type: Sequelize.DATEONLY,
allowNull: false,
defaultValue: '',
validate: {
notEmpty: { msg: 'Expiration date should be set.' },
isDate: { msg: 'Expiration date should be valid.' },
laterThanStarts(val) {
if (moment(val).isSameOrBefore(this.starts)) {
throw new Error('Expiration date should be after the start date.');
}
},
}
},
amount: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: '',
validate: {
notEmpty: { msg: 'Amount should be set.' },
isInt: { msg: 'Amount should be valid.' },
min: { args: [0], msg: 'Amount cannot be negative.' }
}
},
currency: {
type: Sequelize.STRING,
allowNull: false,
validate: {
notEmpty: { msg: 'Currency should be set.' }
}
},
comment: {
type: Sequelize.TEXT,
allowNull: true
},
invoice_name: {
type: Sequelize.TEXT,
allowNull: true
},
invoice_address: {
type: Sequelize.TEXT,
allowNull: true
}
}, {
underscored: true,
tableName: 'payments',
createdAt: 'created_at',
updatedAt: 'updated_at',
});

Payment.beforeValidate(async (payment) => {
// skipping these fields if they are unset, will catch it later.
if (typeof payment.currency === 'string') payment.currency = payment.currency.trim();
if (typeof payment.comment === 'string') payment.comment = payment.comment.trim();
if (typeof payment.invoice_name === 'string') payment.invoice_name = payment.invoice_name.trim();
if (typeof payment.invoice_address === 'string') payment.invoice_address = payment.invoice_address.trim();
});

module.exports = Payment;
10 changes: 9 additions & 1 deletion models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const CirclePermission = require('./CirclePermission');
const CircleMembership = require('./CircleMembership');
const BodyMembership = require('./BodyMembership');
const JoinRequest = require('./JoinRequest');
const Payment = require('./Payment');

Campaign.hasMany(User, { foreignKey: 'campaign_id' });
User.belongsTo(Campaign, { foreignKey: 'campaign_id' });
Expand Down Expand Up @@ -72,6 +73,12 @@ User.hasMany(JoinRequest, { foreignKey: 'user_id' });
JoinRequest.belongsTo(Body, { foreignKey: 'body_id' });
Body.hasMany(JoinRequest, { foreignKey: 'body_id' });

Payment.belongsTo(User, { foreignKey: 'user_id' });
User.hasMany(Payment, { foreignKey: 'user_id' });

Payment.belongsTo(Body, { foreignKey: 'body_id' });
Body.hasMany(Payment, { foreignKey: 'body_id' });

module.exports = {
User,
Campaign,
Expand All @@ -85,5 +92,6 @@ module.exports = {
CirclePermission,
CircleMembership,
BodyMembership,
JoinRequest
JoinRequest,
Payment,
};
Loading

0 comments on commit 5d14e76

Please sign in to comment.