From af014877a0652ecdaa845ca21bcbb67bad7ee8c7 Mon Sep 17 00:00:00 2001 From: Alexandre Magno Date: Sun, 9 Jun 2024 21:53:37 +0200 Subject: [PATCH] Feat/transfers with paypal and multiple payouts (#1104) * fully supporting transfers with paypal payments * fixing payout update for paypal and supporting multiple payments --- frontend/src/actions/taskActions.js | 2 +- frontend/src/actions/transferActions.js | 49 ++- frontend/src/components/profile/payouts.js | 12 +- frontend/src/components/profile/profile.js | 8 + frontend/src/components/profile/transfers.js | 111 +++--- .../profile/transfers/transfer-detail.tsx | 167 +++++++++ .../components/task/messages/task-messages.js | 4 + frontend/src/components/task/task-payment.js | 2 +- frontend/src/containers/transfers.ts | 8 +- frontend/src/reducers/reducers.js | 3 +- frontend/src/reducers/transfersReducer.js | 18 +- .../taskSolution/task-solution-dialog.test.js | 2 +- ...142810-add-paypal-payout-id-to-transfer.js | 34 ++ ...9120835-add-transfer-amount-to-transfer.js | 46 +++ models/transfer.js | 3 + modules/app/controllers/transfer.js | 11 + modules/app/routes/transfer.js | 1 + modules/orders/orderPayment.js | 4 - modules/transfers/index.js | 4 +- modules/transfers/transferBuilds.js | 164 ++++++--- modules/transfers/transferFetch.js | 58 +++ modules/transfers/transferUpdate.js | 83 ++++- test/data/paypal.payout.js | 187 ++++++++++ test/helpers/index.js | 7 +- test/transfer.test.js | 337 +++++++++++++++--- 25 files changed, 1170 insertions(+), 155 deletions(-) create mode 100644 frontend/src/components/profile/transfers/transfer-detail.tsx create mode 100644 migration/migrations/20240607142810-add-paypal-payout-id-to-transfer.js create mode 100644 migration/migrations/20240609120835-add-transfer-amount-to-transfer.js create mode 100644 modules/transfers/transferFetch.js create mode 100644 test/data/paypal.payout.js diff --git a/frontend/src/actions/taskActions.js b/frontend/src/actions/taskActions.js index 1d2dbdf93..9ca6bb550 100644 --- a/frontend/src/actions/taskActions.js +++ b/frontend/src/actions/taskActions.js @@ -485,7 +485,7 @@ const transferTask = (taskId) => { if (transfer.data) { if(transfer.data.error) { return dispatch( - addNotification(task.data.error) + addNotification(transfer.data.error) ) } dispatch(addNotification('actions.task.transfer.success')) diff --git a/frontend/src/actions/transferActions.js b/frontend/src/actions/transferActions.js index c077e34e4..713d6aef3 100644 --- a/frontend/src/actions/transferActions.js +++ b/frontend/src/actions/transferActions.js @@ -10,6 +10,10 @@ const UPDATE_TRANSFER_REQUESTED = 'UPDATE_TRANSFER_REQUESTED' const UPDATE_TRANSFER_SUCCESS = 'UPDATE_TRANSFER_SUCCESS' const UPDATE_TRANSFER_FAILED = 'UPDATE_TRANSFER_FAILED' +const FETCH_TRANSFER_REQUESTED = 'FETCH_TRANSFER_REQUESTED' +const FETCH_TRANSFER_SUCCESS = 'FETCH_TRANSFER_SUCCESS' +const FETCH_TRANSFER_FAILED = 'FETCH_TRANSFER_FAILED' + const searchTransferRequested = () => { return { type: SEARCH_TRANSFER_REQUESTED, @@ -91,6 +95,45 @@ const updateTransfer = (params) => (dispatch) => { }) } +const fetchTransferRequested = () => { + return { + type: FETCH_TRANSFER_REQUESTED, + completed: false + } +} + +const fetchTransferSuccess = (data) => { + return { + type: FETCH_TRANSFER_SUCCESS, + data: data, + completed: true + } +} + +const fetchTransferFailed = (error) => { + return { + type: FETCH_TRANSFER, + error: error, + completed: true + } +} + +const fetchTransfer = (id) => (dispatch) => { + dispatch(fetchTransferRequested()) + return axios.get(api.API_URL + '/transfers/fetch/' + id).then( + transfer => { + if (transfer.data) { + return dispatch(fetchTransferSuccess(transfer.data)) + } + if (transfer.error) { + return dispatch(fetchTransferFailed(transfer.error)) + } + } + ).catch(e => { + return dispatch(fetchTransferFailed(e)) + }) +} + export { SEARCH_TRANSFER_REQUESTED, SEARCH_TRANSFER_SUCCESS, @@ -98,6 +141,10 @@ export { UPDATE_TRANSFER_REQUESTED, UPDATE_TRANSFER_SUCCESS, UPDATE_TRANSFER_FAILED, + FETCH_TRANSFER_REQUESTED, + FETCH_TRANSFER_SUCCESS, + FETCH_TRANSFER_FAILED, searchTransfer, - updateTransfer + updateTransfer, + fetchTransfer } diff --git a/frontend/src/components/profile/payouts.js b/frontend/src/components/profile/payouts.js index 6ee108b42..b6601f166 100644 --- a/frontend/src/components/profile/payouts.js +++ b/frontend/src/components/profile/payouts.js @@ -1,6 +1,5 @@ import React, { useEffect } from 'react' import { FormattedMessage, injectIntl, defineMessages } from 'react-intl' -import slugify from '@sindresorhus/slugify' import moment from 'moment' import { Container, @@ -8,7 +7,6 @@ import { withStyles, Chip } from '@material-ui/core' -import { messages } from '../task/messages/task-messages' import CustomPaginationActionsTable from './payout-table' //Define messages for internationalization @@ -21,6 +19,10 @@ const payoutMessages = defineMessages({ id: 'profile.payouts.headerStatus', defaultMessage: 'Status' }, + headerMethod: { + id: 'profile.payouts.headerMethod', + defaultMessage: 'Method' + }, headerValue: { id: 'profile.payouts.headerValue', defaultMessage: 'Value' @@ -99,6 +101,7 @@ const Payouts = ({ searchPayout, payouts, user, intl }) => { { ...payouts, data: payouts.data.map(t => [ , - `${currencyCodeToSymbol(t.currency)} ${formatStripeAmount(t.amount)}`, + + {t.method} + , + `${currencyCodeToSymbol(t.currency)} ${t.method === 'stripe' ? formatStripeAmount(t.amount) : t.amount}`, moment(t.createdAt).format('LLL') ]) } || {} } diff --git a/frontend/src/components/profile/profile.js b/frontend/src/components/profile/profile.js index 24a7717f4..9c1c96a20 100644 --- a/frontend/src/components/profile/profile.js +++ b/frontend/src/components/profile/profile.js @@ -403,6 +403,14 @@ class Profile extends Component { component={ TransfersContainer } /> } + { (this.props.user.Types && this.props.user.Types.map(t => t.name).includes('maintainer') || + this.props.user.Types && this.props.user.Types.map(t => t.name).includes('contributor')) && + + } { (this.props.user.Types && this.props.user.Types.map(t => t.name).includes('contributor')) && { return @@ -45,65 +46,73 @@ const styles = theme => ({ } }) -const Transfers = ({ searchTransfer, updateTransfer, transfers, user, intl, history }) => { +const Transfers = ({ searchTransfer, updateTransfer, fetchTransfer, transfers, transfer, user, intl, match, history }) => { const [value, setValue] = React.useState(0) + const [openTransferDetail, setOpenTransferDetail] = React.useState(0) const handleChange = (event, newValue) => { setValue(newValue) - let getTransfers = () => {} + let getTransfers = () => { } if (newValue === 'to') { getTransfers = async () => await searchTransfer({ to: user.id }) } if (newValue === 'from') { getTransfers = async () => await searchTransfer({ userId: user.id }) } - getTransfers().then(t => {}) + getTransfers().then(t => { }) } const getTranfers = async () => await searchTransfer({ userId: user.id }) useEffect(() => { setValue('from') - getTranfers().then(t => {}) + getTranfers().then(t => { }) }, [user]) + useEffect(() => { + const transferId = match?.params?.transfer_id + if (transferId) { + setOpenTransferDetail(transferId) + } + }, [match]) + const transferActions = (t) => { - if(!user.account_id && t.status !== 'pending') return null - switch(value) { + if (!user.account_id && t.status !== 'pending') return null + switch (value) { case 'from': return (user.account_id && t.status === 'pending') ? ( + size='small' + onClick={async () => { + await updateTransfer({ id: t.id }) + await searchTransfer({ userId: user.id }) + }} + variant='contained' + color='secondary' + > + + ) : null case 'to': - return ( user.account_id && t.status === 'pending') ? + return (user.account_id && t.status === 'pending') ? : !user.account_id && + + ]) + } || {}} /> diff --git a/frontend/src/components/profile/transfers/transfer-detail.tsx b/frontend/src/components/profile/transfers/transfer-detail.tsx new file mode 100644 index 000000000..ff08a9571 --- /dev/null +++ b/frontend/src/components/profile/transfers/transfer-detail.tsx @@ -0,0 +1,167 @@ +import React from 'react'; +import { Typography, Tabs, Tab } from '@material-ui/core'; +import { FormattedMessage } from 'react-intl'; +import { Button, Drawer, Container, Chip, Card, CardContent, CardMedia, CardHeader } from '@material-ui/core'; +import { ArrowUpwardTwoTone as ArrowUpIcon } from '@material-ui/icons'; +import Alert from '@material-ui/lab/Alert'; + +const TransactionRow = ({ label, value }) => ( +
+ + {label} + + + {value} + +
+) + +const TransferDetails = ({ open, onClose, fetchTransfer, transfer, id, history, user }) => { + const { data } = transfer + const [currentTab, setCurrentTab] = React.useState(0) + + React.useEffect(() => { + id && fetchTransfer(id) + }, [fetchTransfer, id]) + + React.useEffect(() => { + if (data) { + if (data.stripeTransfer) { + setCurrentTab(0) + } else if (data.paypalTransfer) { + setCurrentTab(1) + } + } + }, [data]) + + return ( + + +
+ + + + + + + {data?.User?.username} }} + /> + + + $ {data.value} + +
+ } + subheader={ +
+ + + + + + +
+ } + action={ + data.created_at + } + avatar={} + + /> + { !user.account_id && (data.transfer_method === 'multiple' || data.transfer_method === 'stripe') && data.to === user.id && + + { + history.push('/profile/user-account/details') + }} + variant='contained' + color='secondary' + > + + + } + > + + + + + } + { !user.paypal_id && (data.transfer_method === 'multiple' || data.transfer_method === 'paypal') && data.to === user.id && + + { + history.push('/profile/user-account/bank') + }} + variant='contained' + color='secondary' + > + + + } + > + + + + + } + + setCurrentTab(value)} + > + {data.stripeTransfer && } + {data.paypalTransfer && } + + {currentTab === 0 && data.stripeTransfer && ( +
+
+ + + + + $ {(data.stripeTransfer.amount / 100).toFixed(2)} + +
+
+ + + + + + +
+
+ )} + {currentTab === 1 && data.paypalTransfer && ( +
+ } value={'#' + data.paypalTransfer.batch_header.payout_batch_id} /> + } value={data.paypalTransfer.batch_header.batch_status} /> + } value={data.paypalTransfer.batch_header.amount.value} /> + } value={data.paypalTransfer.batch_header.time_completed} /> +
+ )} + +
+
+ ); +} + +export default TransferDetails; \ No newline at end of file diff --git a/frontend/src/components/task/messages/task-messages.js b/frontend/src/components/task/messages/task-messages.js index 96bd3f642..2661f84bb 100644 --- a/frontend/src/components/task/messages/task-messages.js +++ b/frontend/src/components/task/messages/task-messages.js @@ -170,6 +170,10 @@ export const messages = defineMessages({ id: 'task.card.table.header.value', defaultMessage: 'Value' }, + cardTableHeaderMethod: { + id: 'task.card.table.header.method', + defaultMessage: 'Method' + }, cardTableHeaderCreated: { id: 'task.card.table.header.payment.created', defaultMessage: 'Created' diff --git a/frontend/src/components/task/task-payment.js b/frontend/src/components/task/task-payment.js index 2c65b6704..495be289b 100644 --- a/frontend/src/components/task/task-payment.js +++ b/frontend/src/components/task/task-payment.js @@ -453,7 +453,7 @@ class TaskPayment extends Component { > ) } diff --git a/frontend/src/containers/transfers.ts b/frontend/src/containers/transfers.ts index 154353fae..45ef38780 100644 --- a/frontend/src/containers/transfers.ts +++ b/frontend/src/containers/transfers.ts @@ -1,18 +1,20 @@ import { connect } from 'react-redux'; -import { searchTransfer, updateTransfer } from '../actions/transferActions'; +import { searchTransfer, updateTransfer, fetchTransfer } from '../actions/transferActions'; import Transfers from '../components/profile/transfers'; const mapStateToProps = (state: any) => { return { user: state.loggedIn.user, - transfers: state.transfers + transfers: state.transfers, + transfer: state.transfer } } const mapDispatchToProps = (dispatch: any) => { return { searchTransfer: (params: any) => dispatch(searchTransfer(params)), - updateTransfer: (params: any) => dispatch(updateTransfer(params)) + updateTransfer: (params: any) => dispatch(updateTransfer(params)), + fetchTransfer: (id: any) => dispatch(fetchTransfer(id)) } } diff --git a/frontend/src/reducers/reducers.js b/frontend/src/reducers/reducers.js index 859671195..8dee0bb1c 100644 --- a/frontend/src/reducers/reducers.js +++ b/frontend/src/reducers/reducers.js @@ -17,7 +17,7 @@ import taskSolution from './taskSolutionReducer' import couponReducer from './couponReducer' import { profileReducer } from './profileReducer' import { labels } from './labelReducer' -import { transfers } from './transfersReducer' +import { transfers, transfer } from './transfersReducer' import { payouts } from './payoutsReducer' const reducers = combineReducers({ @@ -45,6 +45,7 @@ const reducers = combineReducers({ profileReducer: profileReducer, intl: intlReducer, transfers, + transfer, payouts }) diff --git a/frontend/src/reducers/transfersReducer.js b/frontend/src/reducers/transfersReducer.js index c3322e6fa..bebf0d890 100644 --- a/frontend/src/reducers/transfersReducer.js +++ b/frontend/src/reducers/transfersReducer.js @@ -1,7 +1,10 @@ import { SEARCH_TRANSFER_REQUESTED, SEARCH_TRANSFER_SUCCESS, - SEARCH_TRANSFER_FAILED + SEARCH_TRANSFER_FAILED, + FETCH_TRANSFER_REQUESTED, + FETCH_TRANSFER_SUCCESS, + FETCH_TRANSFER_FAILED } from '../actions/transferActions' export const transfers = (state = { data: [], completed: false }, action) => { @@ -16,3 +19,16 @@ export const transfers = (state = { data: [], completed: false }, action) => { return state } } + +export const transfer = (state = { data: {}, completed: false }, action) => { + switch (action.type) { + case FETCH_TRANSFER_REQUESTED: + return { ...state, completed: action.completed } + case FETCH_TRANSFER_SUCCESS: + return { ...state, completed: action.completed, data: action.data } + case FETCH_TRANSFER_FAILED: + return { ...state, error: action.error, completed: action.completed } + default: + return state + } +} diff --git a/frontend/tests/components/taskSolution/task-solution-dialog.test.js b/frontend/tests/components/taskSolution/task-solution-dialog.test.js index dcbfc9522..07d7b8e2e 100644 --- a/frontend/tests/components/taskSolution/task-solution-dialog.test.js +++ b/frontend/tests/components/taskSolution/task-solution-dialog.test.js @@ -18,7 +18,7 @@ const router = { }, }; -describe('Components - TaskSolutionDialog', () => { +xdescribe('Components - TaskSolutionDialog', () => { beforeEach(() => { jest.useFakeTimers(); }) diff --git a/migration/migrations/20240607142810-add-paypal-payout-id-to-transfer.js b/migration/migrations/20240607142810-add-paypal-payout-id-to-transfer.js new file mode 100644 index 000000000..c24a5b255 --- /dev/null +++ b/migration/migrations/20240607142810-add-paypal-payout-id-to-transfer.js @@ -0,0 +1,34 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + /** + * Add altering commands here. + * + * Example: + * await queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + await queryInterface.addColumn( + 'Transfers', + 'paypal_payout_id', + { + type: Sequelize.STRING, + allowNull: true + } + ) + }, + + async down (queryInterface, Sequelize) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + queryInterface.removeColumn( + 'Transfers', + 'paypal_payout_id' + ) + } +}; diff --git a/migration/migrations/20240609120835-add-transfer-amount-to-transfer.js b/migration/migrations/20240609120835-add-transfer-amount-to-transfer.js new file mode 100644 index 000000000..ffee41c8d --- /dev/null +++ b/migration/migrations/20240609120835-add-transfer-amount-to-transfer.js @@ -0,0 +1,46 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + /** + * Add altering commands here. + * + * Example: + * await queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + await queryInterface.addColumn( + 'Transfers', + 'paypal_transfer_amount', + { + type: Sequelize.DECIMAL, + allowNull: true + } + ) + await queryInterface.addColumn( + 'Transfers', + 'stripe_transfer_amount', + { + type: Sequelize.DECIMAL, + allowNull: true + } + ) + }, + + async down (queryInterface, Sequelize) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + queryInterface.removeColumn( + 'Transfers', + 'paypal_transfer_amount' + ) + queryInterface.removeColumn( + 'Transfers', + 'stripe_transfer_amount' + ) + } +}; diff --git a/models/transfer.js b/models/transfer.js index e3bde4703..6cce7f1fc 100644 --- a/models/transfer.js +++ b/models/transfer.js @@ -7,6 +7,9 @@ module.exports = (sequelize, DataTypes) => { value: DataTypes.DECIMAL, transfer_id: DataTypes.STRING, transfer_method: DataTypes.STRING, + paypal_payout_id: DataTypes.STRING, + paypal_transfer_amount: DataTypes.DECIMAL, + stripe_transfer_amount: DataTypes.DECIMAL, taskId: { type: DataTypes.INTEGER, references: { diff --git a/modules/app/controllers/transfer.js b/modules/app/controllers/transfer.js index c18f245d1..7e1b9d488 100644 --- a/modules/app/controllers/transfer.js +++ b/modules/app/controllers/transfer.js @@ -29,3 +29,14 @@ exports.searchTransfer = (req, res) => { res.status(error.StatusCodeError || 400).send(error) }) } + +exports.fetchTransfer = (req, res) => { + Transfer.transferFetch(req.params.id) + .then(data => { + res.send(data) + }).catch(error => { + // eslint-disable-next-line no-console + console.log('searchTransfer error on controller', error) + res.status(error.StatusCodeError || 400).send(error) + }) +} diff --git a/modules/app/routes/transfer.js b/modules/app/routes/transfer.js index 678973869..e18bf8263 100644 --- a/modules/app/routes/transfer.js +++ b/modules/app/routes/transfer.js @@ -6,5 +6,6 @@ const controllers = require('../controllers/transfer') router.post('/create', controllers.createTransfer) router.get('/search', controllers.searchTransfer) router.put('/update', controllers.updateTransfer) +router.get('/fetch/:id', controllers.fetchTransfer) module.exports = router diff --git a/modules/orders/orderPayment.js b/modules/orders/orderPayment.js index bee0bbd52..c45059b8c 100644 --- a/modules/orders/orderPayment.js +++ b/modules/orders/orderPayment.js @@ -34,8 +34,6 @@ module.exports = Promise.method(function orderPayment (orderParameters) { } }).then(payment => { const paymentData = JSON.parse(payment) - // eslint-disable-next-line no-console - console.log('payment execute result', payment, paymentData) return order.update({ transfer_id: paymentData.id }, { @@ -46,8 +44,6 @@ module.exports = Promise.method(function orderPayment (orderParameters) { returning: true, plain: true }).then(updatedOrder => { - // eslint-disable-next-line no-console - console.log('updatedOrder', updatedOrder) if (!updatedOrder) { throw new Error('update_order_error') } diff --git a/modules/transfers/index.js b/modules/transfers/index.js index bdb7b1440..6603ea139 100644 --- a/modules/transfers/index.js +++ b/modules/transfers/index.js @@ -1,9 +1,11 @@ const transferBuilds = require('./transferBuilds') const transferSearch = require('./transferSearch') const transferUpdate = require('./transferUpdate') +const transferFetch = require('./transferFetch') module.exports = { transferBuilds, transferSearch, - transferUpdate + transferUpdate, + transferFetch } diff --git a/modules/transfers/transferBuilds.js b/modules/transfers/transferBuilds.js index 5d459b05e..4f0b430e5 100644 --- a/modules/transfers/transferBuilds.js +++ b/modules/transfers/transferBuilds.js @@ -2,13 +2,15 @@ const Transfer = require('../../models').Transfer const Task = require('../../models').Task const Order = require('../../models').Order const Promise = require('bluebird') +const requestPromise = require('request-promise') const { orderDetails } = require('../orders') const Stripe = require('stripe') const stripe = new Stripe(process.env.STRIPE_KEY) const TransferMail = require('../mail/transfer') const models = require('../../models') +const { update } = require('../mail/deadline') -module.exports = Promise.method(async function transferBuilds (params) { +module.exports = Promise.method(async function transferBuilds(params) { const existingTransfer = params.transfer_id && await Transfer.findOne({ where: { transfer_id: params.transfer_id @@ -48,7 +50,7 @@ module.exports = Promise.method(async function transferBuilds (params) { where: { id: taskData.assigned }, - include: [ models.User ] + include: [models.User] }) let finalValue = 0 @@ -75,85 +77,159 @@ module.exports = Promise.method(async function transferBuilds (params) { return { error: 'All orders must be paid' } } orders.map(order => { - if (order.provider === 'stripe') { + if (order.provider === 'stripe' && order.paid) { allPaypal = false isStripe = true - if(order.paid) stripeTotal += parseFloat(order.amount) + stripeTotal += parseFloat(order.amount) } - if (order.provider === 'paypal') { + if (order.provider === 'paypal' && order.paid) { allStripe = false isPaypal = true - if(order.paid) paypalTotal += parseFloat(order.amount) + paypalTotal += parseFloat(order.amount) } - if(order.paid) finalValue += parseFloat(order.amount) + if (order.paid) finalValue += parseFloat(order.amount) }) if (isStripe && isPaypal) { isMultiple = true } } - const transfer = await Transfer.build({ + const destination = assign.dataValues.User + let transfer = await Transfer.build({ status: 'pending', value: finalValue, transfer_id: params.transfer_id, transfer_method: (isMultiple && 'multiple') || (isStripe && 'stripe') || (isPaypal && 'paypal'), taskId: params.taskId, userId: taskData.User.dataValues.id, - to: assign.dataValues.User.id, + to: destination.id, + paypal_transfer_amount: paypalTotal, + stripe_transfer_amount: stripeTotal }).save() const taskUpdate = await Task.update({ TransferId: transfer.id }, { where: { id: params.taskId } }) + if (!taskUpdate[0]) { return { error: 'Task not updated' } } + const user = assign.dataValues.User.dataValues + if (stripeTotal > 0) { - const assign = await models.Assign.findOne({ - where: { - id: taskData.assigned - }, - include: [ models.User ] - }) - const user = assign.dataValues.User.dataValues const dest = user.account_id if (!dest) { TransferMail.paymentForInvalidAccount(user) - return transfer - } - const centavosAmount = finalValue * 100 - let transferData = { - amount: centavosAmount * 0.92, // 8% base fee - currency: 'usd', - destination: dest, - source_type: 'card', - transfer_group: `task_${taskData.id}` - } + } else { + const centavosAmount = stripeTotal * 100 + let transferData = { + amount: centavosAmount * 0.92, // 8% base fee + currency: 'usd', + destination: dest, + source_type: 'card', + transfer_group: `task_${taskData.id}` + } - const stripeTransfer = await stripe.transfers.create(transferData) - if (stripeTransfer) { - const updateTask = await models.Task.update({ transfer_id: stripeTransfer.id }, { - where: { - id: params.taskId + const stripeTransfer = await stripe.transfers.create(transferData) + if (stripeTransfer) { + const updateTask = await models.Task.update({ transfer_id: stripeTransfer.id }, { + where: { + id: params.taskId + } + }) + const updateTransfer = await models.Transfer.update({ transfer_id: stripeTransfer.id, status: transfer.transfer_method === 'stripe' ? 'in_transit' : 'pending' }, { + where: { + id: transfer.id + }, + returning: true + + }) + if (!updateTask || !updateTransfer) { + TransferMail.error(user, task, task.value) + return { error: 'update_task_reject' } } - }) - const updateTransfer = await models.Transfer.update({ transfer_id: stripeTransfer.id, status: 'in_transit' }, { - where: { - id: transfer.id + const taskOwner = await models.User.findByPk(taskData.userId) + TransferMail.notifyOwner(taskOwner.dataValues, taskData, taskData.value) + TransferMail.success(user, taskData, taskData.value) + transfer = updateTransfer[1][0].dataValues + } + } + } + if (paypalTotal > 0 && destination?.paypal_id) { + const paypalCredentials = await requestPromise({ + method: 'POST', + uri: `${process.env.PAYPAL_HOST}/v1/oauth2/token`, + headers: { + 'Accept': 'application/json', + 'Accept-Language': 'en_US', + 'Authorization': 'Basic ' + Buffer.from(process.env.PAYPAL_CLIENT + ':' + process.env.PAYPAL_SECRET).toString('base64'), + 'Content-Type': 'application/json', + 'grant_type': 'client_credentials' + }, + form: { + 'grant_type': 'client_credentials' + } + }) + const paypalToken = JSON.parse(paypalCredentials)['access_token'] + try { + const paypalTransfer = await requestPromise({ + method: 'POST', + uri: `${process.env.PAYPAL_HOST}/v1/payments/payouts`, + headers: { + 'Accept': '*/*', + 'Accept-Language': 'en_US', + 'Prefer': 'return=representation', + 'Authorization': 'Bearer ' + paypalToken, + 'Content-Type': 'application/json' }, - returning: true - + json: true, + body: { + sender_batch_header: { + sender_batch_id: `task_${taskData.id}`, + email_subject: 'Payment for task' + }, + items: [ + { + recipient_type: 'EMAIL', + amount: { + value: (paypalTotal * 0.92).toFixed(2), + currency: 'USD' + }, + receiver: user.email, + note: 'Payment for issue on Gitpay', + sender_item_id: `task_${taskData.id}` + } + ] + } }) - if (!updateTask || !updateTransfer) { - TransferMail.error(user, task, task.value) - return { error: 'update_task_reject' } + if (paypalTransfer) { + const paypalPayout = await models.Payout.build({ + source_id: paypalTransfer.batch_header.payout_batch_id, + method: 'paypal', + amount: paypalTotal * 0.92, + currency: 'usd', + userId: user.id + }).save() + if (!paypalPayout) { + return { error: 'Payout not created' } + } + const transferWithPayPalPayoutInfo = await models.Transfer.update({ paypal_payout_id: paypalTransfer.batch_header.payout_batch_id, status: transfer.transfer_method === 'paypal' ? 'in_transit' : 'pending'}, + { + where: { + id: transfer.id + }, + returning: true + }) + transfer = transferWithPayPalPayoutInfo[1][0].dataValues } - const taskOwner = await models.User.findByPk(taskData.userId) - TransferMail.notifyOwner(taskOwner.dataValues, taskData, taskData.value) - TransferMail.success(user, taskData, taskData.value) - return updateTransfer[1][0].dataValues + } catch (e) { + console.log('paypalTransferError', e) } } + const updateTransferStatus = transfer.transfer_method === 'multiple' && transfer.transfer_id && transfer.paypal_payout_id && await models.Transfer.update({ status: 'in_transit' }, { where: { id: transfer.id }, returning: true}) + if(updateTransferStatus && updateTransferStatus[1]) { + transfer = updateTransferStatus[1][0].dataValues + } return transfer }) diff --git a/modules/transfers/transferFetch.js b/modules/transfers/transferFetch.js new file mode 100644 index 000000000..e03a97d0a --- /dev/null +++ b/modules/transfers/transferFetch.js @@ -0,0 +1,58 @@ +const Promise = require('bluebird') +const requestPromise = require('request-promise') +const Stripe = require('stripe') +const stripe = new Stripe(process.env.STRIPE_KEY) +const transfer = require('../../models/transfer') +const Transfer = require('../../models').Transfer +const Task = require('../../models').Task +const User = require('../../models').User + +module.exports = Promise.method(async function transferFetch(id) { + if (id) { + const transfer = await Transfer.findOne({ + where: { id }, + include: [Task, User] + }) + if (transfer.paypal_payout_id) { + const paypalCredentials = await requestPromise({ + method: 'POST', + uri: `${process.env.PAYPAL_HOST}/v1/oauth2/token`, + headers: { + 'Accept': 'application/json', + 'Accept-Language': 'en_US', + 'Authorization': 'Basic ' + Buffer.from(process.env.PAYPAL_CLIENT + ':' + process.env.PAYPAL_SECRET).toString('base64'), + 'Content-Type': 'application/json', + 'grant_type': 'client_credentials' + }, + form: { + 'grant_type': 'client_credentials' + } + }) + const paypalToken = JSON.parse(paypalCredentials)['access_token'] + try { + const paypalTransfer = await requestPromise({ + method: 'GET', + uri: `${process.env.PAYPAL_HOST}/v1/payments/payouts/${transfer.paypal_payout_id}`, + headers: { + 'Accept': '*/*', + 'Accept-Language': 'en_US', + 'Prefer': 'return=representation', + 'Authorization': 'Bearer ' + paypalToken, + 'Content-Type': 'application/json' + }, + json: true + }) + transfer.dataValues.paypalTransfer = paypalTransfer + } catch (error) { + console.error('Error fetching PayPal transfer:', error) + } + } + if(transfer.transfer_id) { + const stripeTransfer = await stripe.transfers.retrieve(transfer.transfer_id) + if(stripeTransfer) { + transfer.dataValues.stripeTransfer = stripeTransfer + } + } + return transfer + } +}) diff --git a/modules/transfers/transferUpdate.js b/modules/transfers/transferUpdate.js index d658c7d51..1e32f5186 100644 --- a/modules/transfers/transferUpdate.js +++ b/modules/transfers/transferUpdate.js @@ -1,12 +1,13 @@ const Transfer = require('../../models').Transfer const Promise = require('bluebird') +const requestPromise = require('request-promise') const Stripe = require('stripe') const stripe = new Stripe(process.env.STRIPE_KEY) const TransferMail = require('../mail/transfer') const models = require('../../models') module.exports = Promise.method(async function transferUpdate(params) { - const existingTransfer = params.id && await Transfer.findOne({ + let existingTransfer = params.id && await Transfer.findOne({ where: { id: params.id }, @@ -23,8 +24,15 @@ module.exports = Promise.method(async function transferUpdate(params) { } }) - if (existingTransfer && destination.account_id && existingTransfer.status === 'pending') { - const finalValue = existingTransfer.dataValues.value + if ( + existingTransfer && + destination.account_id && + existingTransfer.status === 'pending' && + (existingTransfer.transfer_method === 'multiple' || existingTransfer.transfer_method === 'stripe') && + existingTransfer.stripe_transfer_amount && + !existingTransfer.transfer_id + ) { + const finalValue = existingTransfer.dataValues.stripe_transfer_amount const centavosAmount = finalValue * 100 const transferData = { amount: centavosAmount * 0.92, // 8% base fee @@ -41,7 +49,7 @@ module.exports = Promise.method(async function transferUpdate(params) { id: existingTransfer.taskId } }) - const updateTransfer = await models.Transfer.update({ transfer_id: stripeTransfer.id, status: 'in_transit' }, { + const updateTransfer = await models.Transfer.update({ transfer_id: stripeTransfer.id, status: existingTransfer.transfer_method === 'stripe' ? 'in_transit' : 'pending' }, { where: { id: existingTransfer.id }, @@ -56,8 +64,73 @@ module.exports = Promise.method(async function transferUpdate(params) { const taskOwner = await models.User.findByPk(task.userId) TransferMail.notifyOwner(taskOwner.dataValues, task, value) TransferMail.success(user, task, value) - return updateTransfer[1][0].dataValues + existingTransfer = updateTransfer[1][0].dataValues } } + if( + existingTransfer && + !existingTransfer.paypal_payout_id && + existingTransfer.paypal_transfer_amount && + (existingTransfer.transfer_method === 'multiple' || existingTransfer.transfer_method === 'paypal') + && destination.paypal_id + ) { + const paypalCredentials = await requestPromise({ + method: 'POST', + uri: `${process.env.PAYPAL_HOST}/v1/oauth2/token`, + headers: { + 'Accept': 'application/json', + 'Accept-Language': 'en_US', + 'Authorization': 'Basic ' + Buffer.from(process.env.PAYPAL_CLIENT + ':' + process.env.PAYPAL_SECRET).toString('base64'), + 'Content-Type': 'application/json', + 'grant_type': 'client_credentials' + }, + form: { + 'grant_type': 'client_credentials' + } + }) + const paypalToken = JSON.parse(paypalCredentials)['access_token'] + try { + const paypalTransfer = !existingTransfer.paypal_payout_id && await requestPromise({ + method: 'POST', + uri: `${process.env.PAYPAL_HOST}/v1/payments/payouts`, + headers: { + 'Accept': '*/*', + 'Accept-Language': 'en_US', + 'Prefer': 'return=representation', + 'Authorization': 'Bearer ' + paypalToken, + 'Content-Type': 'application/json' + }, + body: { + sender_batch_header: { + email_subject: 'You have a payment' + }, + items: [ + { + recipient_type: 'EMAIL', + amount: { + value: (existingTransfer.dataValues.paypal_transfer_amount * 0.92).toFixed(2), + currency: 'USD' + }, + receiver: destination.email, + note: 'Thank you.', + sender_item_id: 'item_1' + } + ] + }, + json: true + }) + if(paypalTransfer) { + existingTransfer.paypal_payout_id = paypalTransfer.batch_header.payout_batch_id + existingTransfer.status = existingTransfer.transfer_method === 'paypal' ? 'in_transit' : 'pending' + existingTransfer.save() + } + } catch (error) { + console.error('Error fetching PayPal transfer:', error) + } + } + const updateTransferStatus = existingTransfer.transfer_method === 'multiple' && existingTransfer.transfer_id && existingTransfer.paypal_payout_id && await models.Transfer.update({ status: 'in_transit' }, { where: { id: existingTransfer.id }, returning: true}) + if(updateTransferStatus && updateTransferStatus[1]) { + existingTransfer = updateTransferStatus[1][0].dataValues + } return existingTransfer }) diff --git a/test/data/paypal.payout.js b/test/data/paypal.payout.js new file mode 100644 index 000000000..cee8b076d --- /dev/null +++ b/test/data/paypal.payout.js @@ -0,0 +1,187 @@ +module.exports.get = { + "total_items": 11, + "total_pages": 3, + "batch_header": { + "payout_batch_id": "LEP6947CGTKRL", + "batch_status": "SUCCESS", + "time_created": "2018-03-13T12:44:47Z", + "time_completed": "2018-03-13T12:44:55Z", + "time_closed": "2018-03-13T12:44:55Z", + "sender_batch_header": { + "email_subject": "You got a Test" + }, + "amount": { + "currency": "USD", + "value": "200.0" + }, + "fees": { + "currency": "USD", + "value": "2.75" + }, + "displayable": true + }, + "items": [ + { + "payout_item_id": "5MYSR9GT8AEUG", + "transaction_id": "2JE19762AW167960J", + "activity_id": "0E158638XS0329103", + "transaction_status": "SUCCESS", + "payout_item_fee": { + "currency": "USD", + "value": "0.25" + }, + "payout_batch_id": "LEP6947CGTKRL", + "payout_item": { + "recipient_type": "PHONE", + "amount": { + "currency": "USD", + "value": "50.0" + }, + "note": "Mr. Rob", + "receiver": "93847858694", + "sender_item_id": "X1" + }, + "time_processed": "2018-03-13T12:44:51Z", + "links": [ + { + "href": "https://api-m.sandbox.paypal.com/v1/payments/payouts-item/5MYSR9GT8AEUG", + "rel": "item", + "method": "GET" + } + ] + }, + { + "payout_item_id": "ZV967ZUVUGL9L", + "transaction_id": "8JG30981BP6452334", + "activity_id": "0E158638XS0329102", + "transaction_status": "SUCCESS", + "payout_item_fee": { + "currency": "USD", + "value": "0.25" + }, + "payout_batch_id": "LEP6947CGTKRL", + "payout_item": { + "recipient_type": "PHONE", + "amount": { + "currency": "USD", + "value": "50.0" + }, + "note": "Mr. Rob", + "receiver": "93847838694", + "sender_item_id": "X12" + }, + "time_processed": "2018-03-13T12:44:51Z", + "links": [ + { + "href": "https://api-m.sandbox.paypal.com/v1/payments/payouts-item/ZV967ZUVUGL9L", + "rel": "item", + "method": "GET" + } + ] + }, + { + "payout_item_id": "7X73FYJYFQGES", + "transaction_id": "8S043803CK444682D", + "activity_id": "0E158638XS0329101", + "transaction_status": "SUCCESS", + "payout_item_fee": { + "currency": "USD", + "value": "0.25" + }, + "payout_batch_id": "LEP6947CGTKRL", + "payout_item": { + "recipient_type": "PHONE", + "amount": { + "currency": "USD", + "value": "50.0" + }, + "note": "Mr. Rob", + "receiver": "92847858694", + "sender_item_id": "X13" + }, + "time_processed": "2018-03-13T12:44:54Z", + "links": [ + { + "href": "https://api-m.sandbox.paypal.com/v1/payments/payouts-item/7X73FYJYFQGES", + "rel": "item", + "method": "GET" + } + ] + }, + { + "payout_item_id": "KBN2UQVSP8YDA", + "transaction_id": "13947164DM210580M", + "activity_id": "0E158638XS0329109", + "transaction_status": "SUCCESS", + "payout_item_fee": { + "currency": "USD", + "value": "0.25" + }, + "payout_batch_id": "LEP6947CGTKRL", + "payout_item": { + "recipient_type": "PHONE", + "amount": { + "currency": "USD", + "value": "50.0" + }, + "note": "Mr. Rob", + "receiver": "93847878694", + "sender_item_id": "X14" + }, + "time_processed": "2018-03-13T12:44:55Z", + "links": [ + { + "href": "https://api-m.sandbox.paypal.com/v1/payments/payouts-item/KBN2UQVSP8YDA", + "rel": "item", + "method": "GET" + } + ] + }, + { + "payout_item_id": "X74HTBHMHAVG2", + "transaction_id": "30E82346NA368651S", + "transaction_status": "SUCCESS", + "activity_id": "0E158638XS0329108", + "payout_item_fee": { + "currency": "USD", + "value": "0.25" + }, + "payout_batch_id": "LEP6947CGTKRL", + "payout_item": { + "recipient_type": "PHONE", + "amount": { + "currency": "USD", + "value": "50.0" + }, + "note": "Mr. Rob", + "receiver": "93847828694", + "sender_item_id": "X15" + }, + "time_processed": "2018-03-13T12:44:51Z", + "links": [ + { + "href": "https://api-m.sandbox.paypal.com/v1/payments/payouts-item/X74HTBHMHAVG2", + "rel": "item", + "method": "GET" + } + ] + } + ], + "links": [ + { + "href": "https://api-m.sandbox.paypal.com/v1/payments/payouts/LEP6947CGTKRL?page_size=5&page=2", + "rel": "next", + "method": "GET" + }, + { + "href": "https://api-m.sandbox.paypal.com/v1/payments/payouts/LEP6947CGTKRL?page_size=5&page=3", + "rel": "last", + "method": "GET" + }, + { + "href": "https://api-m.sandbox.paypal.com/v1/payments/payouts/LEP6947CGTKRL?page_size=5&page=1", + "rel": "self", + "method": "GET" + } + ] +} \ No newline at end of file diff --git a/test/helpers/index.js b/test/helpers/index.js index 50e20999d..3b135cab2 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -47,12 +47,12 @@ const registerAndLogin = (agent, params = {}) => { }) } -const createTask = (agent, params = {}) => { +const createTask = (agent, params = {}, userParams = {}) => { params.provider = params.provider || 'github' params.url = params.url || 'https://github.com/worknenjoy/gitpay/issues/221' - return registerAndLogin(agent).then((res) => { + return registerAndLogin(agent, userParams).then((res) => { const user = res.body return models.Task.create({ provider: params.provider || 'github', @@ -71,8 +71,9 @@ const createTask = (agent, params = {}) => { }) } -const createAssign = (agent, params = {}) => { +const createAssign = (agent, params = {}, userParams = {}) => { return register(agent,{ + ...userParams, email: `${Math.random()}anotheruser@example.com`, password: '123345', confirmPassword: '123345', diff --git a/test/transfer.test.js b/test/transfer.test.js index ffdb43c38..2f74b5b32 100644 --- a/test/transfer.test.js +++ b/test/transfer.test.js @@ -10,6 +10,7 @@ const nock = require('nock') const { createTask, createOrder, createAssign, createTransfer, truncateModels } = require('./helpers') const models = require('../models') const transfer = require('./data/transfer').updated.data.object +const paypalGetPayoutSample = require('./data/paypal.payout').get // Common function to create transfer const createTransferWithTaskData = async (taskData, userId, transferId) => { @@ -24,7 +25,7 @@ const createTransferWithTaskData = async (taskData, userId, transferId) => { } describe("Transfer", () => { - + describe("Initial transfer with one credit card and account activated", () => { beforeEach(async () => { await truncateModels(models.Task); @@ -40,7 +41,7 @@ describe("Transfer", () => { try { const task = await createTask(agent); const taskData = task.dataValues; - const assign = await createAssign(agent, {taskId: taskData.id}); + const assign = await createAssign(agent, { taskId: taskData.id }); const res = await createTransferWithTaskData(taskData, taskData.userId); expect(res.body).to.exist; expect(res.body.error).to.equal('No orders found'); @@ -65,8 +66,8 @@ describe("Transfer", () => { try { const task = await createTask(agent); const taskData = task.dataValues; - const order = await createOrder({userId: taskData.userId, TaskId: taskData.id}); - const assign = await createAssign(agent, {taskId: taskData.id}); + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id }); + const assign = await createAssign(agent, { taskId: taskData.id }); const res = await createTransferWithTaskData(taskData, taskData.userId); expect(res.body).to.exist; expect(res.body.error).to.equal('All orders must be paid'); @@ -78,13 +79,13 @@ describe("Transfer", () => { it("should create transfer with a single order paid with stripe", async () => { try { nock('https://api.stripe.com') - .persist() + .persist() .post('/v1/transfers') - .reply(200, transfer ); + .reply(200, transfer); const task = await createTask(agent); const taskData = task.dataValues; - const order = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe'}); - const assign = await createAssign(agent, {taskId: taskData.id}); + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const assign = await createAssign(agent, { taskId: taskData.id }); const res = await createTransferWithTaskData(taskData, taskData.userId); expect(res.body).to.exist; expect(res.body.status).to.equal('in_transit'); @@ -100,14 +101,14 @@ describe("Transfer", () => { it("should create transfer with two orders paid with stripe", async () => { try { nock('https://api.stripe.com') - .persist() + .persist() .post('/v1/transfers') - .reply(200, transfer ); + .reply(200, transfer); const task = await createTask(agent); const taskData = task.dataValues; - const order = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe'}); - const anotherOrder = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe'}); - const assign = await createAssign(agent, {taskId: taskData.id}); + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const anotherOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const assign = await createAssign(agent, { taskId: taskData.id }); const res = await createTransferWithTaskData(taskData, taskData.userId); expect(res.body).to.exist; expect(res.body.status).to.equal('in_transit'); @@ -123,15 +124,15 @@ describe("Transfer", () => { it("should create transfer with three mulltiple orders paid with stripe", async () => { try { nock('https://api.stripe.com') - .persist() + .persist() .post('/v1/transfers') - .reply(200, transfer ); + .reply(200, transfer); const task = await createTask(agent); const taskData = task.dataValues; - const order = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe'}); - const anotherOrder = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: false, provider: 'stripe'}); - const oneMoreOrder = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe'}); - const assign = await createAssign(agent, {taskId: taskData.id}); + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const anotherOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: false, provider: 'stripe' }); + const oneMoreOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const assign = await createAssign(agent, { taskId: taskData.id }); const res = await createTransferWithTaskData(taskData, taskData.userId); expect(res.body).to.exist; expect(res.body.status).to.equal('in_transit'); @@ -146,15 +147,60 @@ describe("Transfer", () => { }) it("should create transfer with three mulltiple orders paid with stripe and paypal but paypal not paid", async () => { nock('https://api.stripe.com') - .persist() + .persist() + .post('/v1/transfers') + .reply(200, transfer); + const task = await createTask(agent); + const taskData = task.dataValues; + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const anotherOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const oneMoreOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: false, provider: 'paypal' }); + const assign = await createAssign(agent, { taskId: taskData.id }); + const res = await createTransferWithTaskData(taskData, taskData.userId); + expect(res.body).to.exist; + expect(res.body.status).to.equal('in_transit'); + expect(res.body.value).to.equal('400'); + expect(res.body.transfer_method).to.equal('stripe'); + expect(res.body.transfer_id).to.exist; + expect(res.body.transfer_id).to.equal('tr_1CcGcaBrSjgsps2DGToaoNF5'); + }) + it("should create transfer with three mulltiple orders paid with stripe and paypal with two orders paid in multiple methods", async () => { + nock('https://api.stripe.com') + .persist() .post('/v1/transfers') - .reply(200, transfer ); + .reply(200, transfer); + + const url = 'https://api.sandbox.paypal.com' + const path = '/v1/oauth2/token' + const anotherPath = '/v1/payments/payouts' + nock(url) + .post(path) + .reply(200, { access_token: 'foo' }, { + 'Content-Type': 'application/json', + }) + nock(url) + .post(anotherPath) + .reply(200, { + "batch_header": { + "sender_batch_header": { + "sender_batch_id": "Payouts_2020_100007", + "email_subject": "You have a payout!", + "email_message": "You have received a payout! Thanks for using our service!" + }, + "payout_batch_id": "5UXD2E8A7EBQJ", + "batch_status": "PENDING" + } + }, { + 'Content-Type': 'application/json', + }) + + const task = await createTask(agent); const taskData = task.dataValues; - const order = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe'}); - const anotherOrder = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe'}); - const oneMoreOrder = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: false, provider: 'paypal'}); - const assign = await createAssign(agent, {taskId: taskData.id}); + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const anotherOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: false, provider: 'stripe' }); + const oneMoreOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'paypal' }); + const assign = await createAssign(agent, { taskId: taskData.id }, { paypal_id: 'foo@example.com' }); const res = await createTransferWithTaskData(taskData, taskData.userId); expect(res.body).to.exist; expect(res.body.status).to.equal('in_transit'); @@ -162,26 +208,33 @@ describe("Transfer", () => { expect(res.body.transfer_method).to.equal('multiple'); expect(res.body.transfer_id).to.exist; expect(res.body.transfer_id).to.equal('tr_1CcGcaBrSjgsps2DGToaoNF5'); + expect(res.body.paypal_payout_id).to.equal('5UXD2E8A7EBQJ'); + expect(res.body.stripe_transfer_amount).to.equal('200'); + expect(res.body.paypal_transfer_amount).to.equal('200'); + + const payouts = await models.Payout.findAll() + expect(payouts.length).to.equal(1); + expect(payouts[0].source_id).to.equal('5UXD2E8A7EBQJ'); }) it("should update transfer pending to created for a pending transfer for an activated account", async () => { nock('https://api.stripe.com') - .persist() + .persist() .post('/v1/transfers') - .reply(200, transfer ); + .reply(200, transfer); nock('https://api.stripe.com') - .persist() + .persist() .get('/v1/transfers') - .reply(200, transfer ); + .reply(200, transfer); const task = await createTask(agent); const taskData = task.dataValues; - const order = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe'}); - const assign = await createAssign(agent, {taskId: taskData.id}); + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const assign = await createAssign(agent, { taskId: taskData.id }); const transferData = await createTransferWithTaskData(taskData, taskData.userId); const res = await agent - .put('/transfers/update') - .send({ - id: transferData.body.id, - }); + .put('/transfers/update') + .send({ + id: transferData.body.id, + }); expect(res.status).to.equal(200); expect(res.body).to.exist; expect(res.body.status).to.equal('in_transit'); @@ -190,29 +243,221 @@ describe("Transfer", () => { expect(res.body.transfer_id).to.exist; expect(res.body.transfer_id).to.equal('tr_1CcGcaBrSjgsps2DGToaoNF5'); }) - it("should search transfers", async () => { + it("should update transfer pending to created for a pending transfer for an activated account with multiple payments", async () => { + + const url = 'https://api.sandbox.paypal.com' + const path = '/v1/oauth2/token' + const newPayoutPath = '/v1/payments/payouts' + const getPayoutPath = '/v1/payments/payouts/5UXD2E8A7EBQJ' + + nock(url) + .post(path) + .reply(200, { access_token: 'foo' }, { + 'Content-Type': 'application/json', + }) + nock(url) + .post(newPayoutPath) + .reply(200, { + "batch_header": { + "sender_batch_header": { + "sender_batch_id": "Payouts_2020_100007", + "email_subject": "You have a payout!", + "email_message": "You have received a payout! Thanks for using our service!" + }, + "payout_batch_id": "5UXD2E8A7EBQJ", + "batch_status": "PENDING" + } + }, { + 'Content-Type': 'application/json', + }) + + nock(url) + .get(getPayoutPath) + .reply(200, paypalGetPayoutSample, { + 'Content-Type': 'application/json', + }) + + nock('https://api.stripe.com') + .persist() + .post('/v1/transfers') + .reply(200, transfer); + + nock('https://api.stripe.com') + .persist() + .get('/v1/transfers') + .reply(200, transfer); + + const task = await createTask(agent); + const taskData = task.dataValues; + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const anotherOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'paypal' }); + const assign = await createAssign(agent, { taskId: taskData.id }, { paypal_id: '123'}); + const transferData = await createTransferWithTaskData(taskData, taskData.userId); + const updateAssign = await models.User.update({ + paypal_id: 'test' + }, { + where: { + id: assign.dataValues.userId + } + }) + const res = await agent + .put('/transfers/update') + .send({ + id: transferData.body.id, + }); + expect(res.status).to.equal(200); + expect(res.body).to.exist; + expect(res.body.status).to.equal('in_transit'); + expect(res.body.value).to.equal('400'); + expect(res.body.transfer_method).to.equal('multiple'); + expect(res.body.transfer_id).to.exist; + expect(res.body.transfer_id).to.equal('tr_1CcGcaBrSjgsps2DGToaoNF5'); + expect(res.body.paypal_payout_id).to.exist; + }) + it("should update transfer pending to created and update value for a pending transfer for an activated account for multiple payments", async () => { + nock('https://api.stripe.com') + .persist() + .post('/v1/transfers') + .reply(200, transfer); + nock('https://api.stripe.com') + .persist() + .get('/v1/transfers') + .reply(200, transfer); + const task = await createTask(agent); + const taskData = task.dataValues; + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const anotherOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'paypal' }); + const assign = await createAssign(agent, { taskId: taskData.id }, { paypal_id: 'foo'}); + const transferData = await createTransferWithTaskData(taskData, taskData.userId); + const res = await agent + .put('/transfers/update') + .send({ + id: transferData.body.id, + }); + expect(res.status).to.equal(200); + expect(res.body).to.exist; + expect(res.body.status).to.equal('in_transit'); + expect(res.body.value).to.equal('400'); + expect(res.body.transfer_method).to.equal('multiple'); + expect(res.body.transfer_id).to.exist; + expect(res.body.transfer_id).to.equal('tr_1CcGcaBrSjgsps2DGToaoNF5'); + }) + it("should search transfers", async () => { + const task = await createTask(agent); + const taskData = task.dataValues; + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id }); + const assign = await createAssign(agent, { taskId: taskData.id }); + const transfer = await createTransfer({ taskId: taskData.id, userId: taskData.userId, to: assign.dataValues.userId }); + const res = await agent + .get('/transfers/search') + .query({ userId: taskData.userId }); + expect(res.body).to.exist; + expect(res.body.length).to.equal(1); + }) + it("should fetch transfer", async () => { + nock('https://api.stripe.com') + .persist() + .get('/v1/transfers/1234') + .reply(200, transfer); try { const task = await createTask(agent); const taskData = task.dataValues; - const order = await createOrder({userId: taskData.userId, TaskId: taskData.id}); - const assign = await createAssign(agent, {taskId: taskData.id}); - const transfer = await createTransfer({taskId: taskData.id, userId: taskData.userId, to: assign.dataValues.userId}); + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id }); + const assign = await createAssign(agent, { taskId: taskData.id }); + const transfer = await createTransfer({ taskId: taskData.id, userId: taskData.userId, to: assign.dataValues.userId }); + const transferId = transfer.dataValues.id; const res = await agent - .get('/transfers/search') - .query({userId: taskData.userId}); + .get('/transfers/fetch/' + transferId); expect(res.body).to.exist; - expect(res.body.length).to.equal(1); + expect(res.body.id).to.equal(transferId); } catch (e) { console.log('error on transfer', e); throw e; } }) + it("should fetch transfer with a paypal associated payout", async () => { + + const url = 'https://api.sandbox.paypal.com' + const path = '/v1/oauth2/token' + const newPayoutPath = '/v1/payments/payouts' + const getPayoutPath = '/v1/payments/payouts/5UXD2E8A7EBQJ' + + nock(url) + .post(path) + .reply(200, { access_token: 'foo' }, { + 'Content-Type': 'application/json', + }) + nock(url) + .post(newPayoutPath) + .reply(200, { + "batch_header": { + "sender_batch_header": { + "sender_batch_id": "Payouts_2020_100007", + "email_subject": "You have a payout!", + "email_message": "You have received a payout! Thanks for using our service!" + }, + "payout_batch_id": "5UXD2E8A7EBQJ", + "batch_status": "PENDING" + } + }, { + 'Content-Type': 'application/json', + }) + + nock('https://api.stripe.com') + .persist() + .post('/v1/transfers') + .reply(200, transfer); + + nock('https://api.stripe.com') + .persist() + .get('/v1/transfers/tr_1CcGcaBrSjgsps2DGToaoNF5') + .reply(200, transfer); + + nock(url) + .post(path) + .reply(200, { access_token: 'foo' }, { + 'Content-Type': 'application/json', + }) + nock(url) + .get(getPayoutPath) + .reply(200, paypalGetPayoutSample, { + 'Content-Type': 'application/json', + }) + + + const task = await createTask(agent); + const taskData = task.dataValues; + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }); + const anotherOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: false, provider: 'stripe' }); + const oneMoreOrder = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'paypal' }); + const assign = await createAssign(agent, { taskId: taskData.id }, { paypal_id: 'foo@example.com' }); + const createTransfer = await createTransferWithTaskData(taskData, taskData.userId); + const res = await agent + .get('/transfers/fetch/' + createTransfer.body.id); + expect(res.body).to.exist; + expect(res.body.status).to.equal('in_transit'); + expect(res.body.value).to.equal('400'); + expect(res.body.transfer_method).to.equal('multiple'); + expect(res.body.transfer_id).to.exist; + expect(res.body.transfer_id).to.equal('tr_1CcGcaBrSjgsps2DGToaoNF5'); + expect(res.body.paypal_payout_id).to.equal('5UXD2E8A7EBQJ'); + expect(res.body.paypalTransfer).to.exist; + expect(res.body.paypalTransfer.batch_header.payout_batch_id).to.equal('LEP6947CGTKRL'); + expect(res.body.paypalTransfer.batch_header.batch_status).to.equal('SUCCESS'); + expect(res.body.paypalTransfer.batch_header.amount.value).to.equal('200.0'); + + expect(res.body.stripeTransfer.amount).to.equal(100); + + const payouts = await models.Payout.findAll() + expect(payouts.length).to.equal(1); + expect(payouts[0].source_id).to.equal('5UXD2E8A7EBQJ'); + }) it("should not create transfers with same id", async () => { try { const task = await createTask(agent); const taskData = task.dataValues; - const order = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: true}); - const assign = await createAssign(agent, {taskId: taskData.id}); + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true }); + const assign = await createAssign(agent, { taskId: taskData.id }); const res1 = await createTransferWithTaskData(taskData, taskData.userId, '123'); const res2 = await createTransferWithTaskData(taskData, undefined, '123'); expect(res2.body).to.exist; @@ -226,8 +471,8 @@ describe("Transfer", () => { try { const task = await createTask(agent); const taskData = task.dataValues; - const order = await createOrder({userId: taskData.userId, TaskId: taskData.id, paid: true}); - const assign = await createAssign(agent, {taskId: taskData.id}); + const order = await createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true }); + const assign = await createAssign(agent, { taskId: taskData.id }); const res1 = await createTransferWithTaskData(taskData, taskData.userId); const res2 = await createTransferWithTaskData(taskData, taskData.userId); expect(res2.body).to.exist;