From 29b716eaa39bbeb2fb48431a3494c066f1b73dde Mon Sep 17 00:00:00 2001 From: Alexandre Magno Date: Tue, 16 Jan 2024 23:32:51 +0100 Subject: [PATCH] Feat/transfer model (#1039) * initial transfer model to track transfers of bounties * adding one more attribute to the transfer model * transfer basic tests * tests and basic api for async transfers * adding transfer section and refactoring payments section to functional component * getting a basic transfer list * getting the basic transfer list and transfer cases starting by a simple order * transfer needs now more attributes * finishing up with the transfer webhook updating transfer * update transfer webhook * fixing wrong bank codes * fixing lint transfers --- frontend/src/actions/taskActions.js | 56 +- frontend/src/actions/transferActions.js | 53 ++ frontend/src/bank-codes-br.js | 87 ++-- frontend/src/components/profile/payments.js | 480 +++++++++--------- .../components/profile/profile-sidebar.tsx | 26 +- frontend/src/components/profile/profile.js | 9 + .../src/components/profile/transfer-table.js | 249 +++++++++ frontend/src/components/profile/transfers.js | 105 ++++ .../components/task/messages/task-messages.js | 4 + frontend/src/components/task/task-payment.js | 76 +-- frontend/src/components/task/task.js | 1 + frontend/src/components/welcome/welcome.js | 18 +- frontend/src/containers/task.js | 3 +- frontend/src/containers/transfers.ts | 18 + frontend/src/reducers/reducers.js | 4 +- frontend/src/reducers/taskReducer.js | 1 + frontend/src/reducers/transfersReducer.js | 18 + .../20240102224715-create-transfer.js | 45 ++ .../20240105133559-add-transferId-to-task.js | 38 ++ .../20240111183036-add-userId-to-transfer.js | 38 ++ .../20240111183241-add-to-to-transfer.js | 38 ++ models/task.js | 9 + models/transfer.js | 48 ++ modules/app/controllers/transfer.js | 20 + modules/app/controllers/webhook.js | 63 ++- modules/app/index.js | 2 + modules/app/routes/transfer.js | 9 + modules/tasks/taskFetch.js | 5 + modules/transfers/index.js | 7 + modules/transfers/transferBuilds.js | 153 ++++++ modules/transfers/transferSearch.js | 22 + package-lock.json | 367 ++++++------- package.json | 3 +- test/data/balance.transaction.js | 17 + test/data/transfer.js | 2 +- test/helpers/index.js | 103 +++- test/task.test.js | 2 +- test/transfer.test.js | 143 +++++- test/webhook.test.js | 80 ++- 39 files changed, 1802 insertions(+), 620 deletions(-) create mode 100644 frontend/src/actions/transferActions.js create mode 100644 frontend/src/components/profile/transfer-table.js create mode 100644 frontend/src/components/profile/transfers.js create mode 100644 frontend/src/containers/transfers.ts create mode 100644 frontend/src/reducers/transfersReducer.js create mode 100644 migration/migrations/20240102224715-create-transfer.js create mode 100644 migration/migrations/20240105133559-add-transferId-to-task.js create mode 100644 migration/migrations/20240111183036-add-userId-to-transfer.js create mode 100644 migration/migrations/20240111183241-add-to-to-transfer.js create mode 100644 models/transfer.js create mode 100644 modules/app/controllers/transfer.js create mode 100644 modules/app/routes/transfer.js create mode 100644 modules/transfers/index.js create mode 100644 modules/transfers/transferBuilds.js create mode 100644 modules/transfers/transferSearch.js create mode 100644 test/data/balance.transaction.js diff --git a/frontend/src/actions/taskActions.js b/frontend/src/actions/taskActions.js index bd8c9e796..9bf31e0ba 100644 --- a/frontend/src/actions/taskActions.js +++ b/frontend/src/actions/taskActions.js @@ -59,6 +59,10 @@ const CLAIM_TASK_REQUESTED = 'CLAIM_TASK_REQUESTED' const CLAIM_TASK_SUCCESS = 'CLAIM_TASK_SUCCESS' const CLAIM_TASK_ERROR = 'CLAIM_TASK_ERROR' +const TRANSFER_TASK_REQUESTED = 'TRANSFER_TASK_REQUESTED' +const TRANSFER_TASK_SUCCESS = 'TRANSFER_TASK_SUCCESS' +const TRANSFER_TASK_ERROR = 'TRANSFER_TASK_ERROR' + const VALIDATION_ERRORS = { 'url must be unique': 'actions.task.create.validation.url', 'Not Found': 'actions.task.create.validation.invalid' @@ -446,6 +450,55 @@ const fetchTask = taskId => { } } +/* Transfer Task */ + +const transferTaskRequested = () => { + return { type: TRANSFER_TASK_REQUESTED, completed: false } +} + +const transferTaskSuccess = task => { + return { type: TRANSFER_TASK_SUCCESS, completed: true, data: task.data } +} + +const transferTaskError = error => { + return { type: TRANSFER_TASK_ERROR, completed: true, error: error } +} + +const transferTask = (taskId) => { + return dispatch => { + dispatch(transferTaskRequested()) + axios + .post(api.API_URL + `/transfers/create`, { + taskId + }) + .then(transfer => { + if (transfer.data) { + if(transfer.data.error) { + return dispatch( + addNotification(task.data.error) + ) + } + dispatch(addNotification('actions.task.transfer.success')) + dispatch(transferTaskSuccess(transfer)) + return dispatch(fetchTask(taskId)) + } + return dispatch( + transferTaskError({ message: 'actions.task.transfer.unavailable' }) + ) + }) + .catch(e => { + dispatch( + addNotification('actions.task.transfer.other.error') + ) + dispatch(transferTaskError(e)) + // eslint-disable-next-line no-console + console.log('not possible to transfer issue') + // eslint-disable-next-line no-console + console.log(e) + }) + } +} + const paymentTask = (taskId, value) => { return (dispatch, getState) => { dispatch(paymentTaskRequested()) @@ -776,5 +829,6 @@ export { fundingInviteTask, changeTaskTab, reportTask, - requestClaimTask + requestClaimTask, + transferTask } diff --git a/frontend/src/actions/transferActions.js b/frontend/src/actions/transferActions.js new file mode 100644 index 000000000..307126a98 --- /dev/null +++ b/frontend/src/actions/transferActions.js @@ -0,0 +1,53 @@ +import axios from 'axios' +import api from '../consts' + +const SEARCH_TRANSFER_REQUESTED = 'SEARCH_TRANSFER_REQUESTED' +const SEARCH_TRANSFER_SUCCESS = 'SEARCH_TRANSFER_SUCCESS' +const SEARCH_TRANSFER_FAILED = 'SEARCH_TRANSFER_FAILED' + +const searchTransferRequested = () => { + return { + type: SEARCH_TRANSFER_REQUESTED, + completed: false + + } +} + +const searchTransferSuccess = (data) => { + return { + type: SEARCH_TRANSFER_SUCCESS, + data: data, + completed: true + } +} + +const searchTransferFailed = (error) => { + return { + type: SEARCH_TRANSFER_FAILED, + error: error, + completed: true + } +} + +const searchTransfer = (params) => (dispatch) => { + dispatch(searchTransferRequested()) + return axios.get(api.API_URL + '/transfers/search', { params }).then( + transfer => { + if (transfer.data) { + return dispatch(searchTransferSuccess(transfer.data)) + } + if (transfer.error) { + return dispatch(searchTransferFailed(transfer.error)) + } + } + ).catch(e => { + return dispatch(searchTransferFailed(e)) + }) +} + +export { + SEARCH_TRANSFER_REQUESTED, + SEARCH_TRANSFER_SUCCESS, + SEARCH_TRANSFER_FAILED, + searchTransfer +} diff --git a/frontend/src/bank-codes-br.js b/frontend/src/bank-codes-br.js index bd4a5e3d6..cd0eff1ab 100644 --- a/frontend/src/bank-codes-br.js +++ b/frontend/src/bank-codes-br.js @@ -1,45 +1,44 @@ -export default BANK_CODES: { - '110': 'BANCO PARA TESTE STRIPE', - '260': 'NUBANK', - '001': 'BANCO DO BRASIL S.A. (Banco do Brasil)', - '237': 'BANCO BRADESCO S.A. (Bradesco)', - '341': 'BANCO ITAU S.A. (Itaú)', - '033': 'BANCO SANTANDER (BRASIL) S.A.', - '409': 'UNIBANCO UNIAO DE BANCOS BRASILEIROS S.A. (Unibanco)', - '041': 'BANCO DO ESTADO DO RIO GRANDE DO SUL S.A. (Banrisul)', - '104': 'CAIXA ECONOMICA FEDERAL (Caixa Econômica Federal)', - '399': 'HSBC BANK BRASIL S.A.BANCO MULTIPLO (HSBC)', - '745': 'BANCO CITIBANK S.A.', - '151': 'BANCO NOSSA CAIXA S.A (Nossa Caixa)', - '389': 'BANCO MERCANTIL DO BRASIL S.A. (Mercantil do Brasil)', - '004': 'BANCO DO NORDESTE DO BRASIL S.A (Banco do Nordeste (BNB) )', - '021': 'BANESTES S.A BANCO DO ESTADO DO ESPIRITO SANTO (Banestes)', - '422': 'BANCO SAFRA S.A. (Safra)', - '003': 'BANCO DA AMAZONIA S.A. (Banco da Amazônia (Basa))', - '047': 'Banco do Estado de Sergipe S.A (Banese)', - '070': 'Banco de Brasília S.A. (BRB)', - '655': 'Banco Votorantim S.A (Votorantim)', - '107': 'Banco BBM S.A (BBM)', - '025': 'Banco Alfa S.A (Alfa)', - '263': 'Banco Cacique S. A. (Cacique)', - '229': 'BANCO CRUZEIRO DO SUL S.A. (Cruzeiro do Sul)', - '252': 'BANCO FININVEST S.A. (Fininvest)', - '063': 'BANCO IBI S.A - BANCO MULTIPLO (Banco IBI)', - '623': 'BANCO PANAMERICANO S.A. (PanAmericano)', - '633': 'BANCO RENDIMENTO S.A. (Banco Rendimento)', - '749': 'BANCO SIMPLES S.A. (Banco Simples)', - '215': 'BANCO ACOMERCIAL E DE INVESTIMENTO SUDAMERIS S.A. (Sudameris)', - '756': 'BANCO COOPERATIVO DO BRASIL S.A. - (BANCOOB)', - '748': 'BANCO COOPERATIVO SICREDI S.A. (SICREDI)', - '065': 'LEMON BANK BANCO MÚLTIPLO S..A (Lemon Bank)', - '069': 'BPN BRASIL BANCO MÚLTIPLO S.A. (BPN)', - '719': 'BANIF - BANCO INTERNACIONAL DO FUNCHAL (BRASIL), S.A. (Banif)', - '318': 'BANCO BMG S.A. (BMG)', - '027': 'BANCO DO ESTADO DE SANTA CATARINA S.A.', - '208': 'BANCO UBS PACTUAL S.A.', - '479': 'BANCO ITAUBANK S.A.', - '077': 'BANCO INTERMEDIUM S.A.', - '212': 'BANCO ORIGINAL', - '085': 'CECRED-COOPERATIVA CENTRAL DE CREDITO URBANO' - } +export default BANK_CODES = { + '110': 'BANCO PARA TESTE STRIPE', + '260': 'NUBANK', + '001': 'BANCO DO BRASIL S.A. (Banco do Brasil)', + '237': 'BANCO BRADESCO S.A. (Bradesco)', + '341': 'BANCO ITAU S.A. (Itaú)', + '033': 'BANCO SANTANDER (BRASIL) S.A.', + '409': 'UNIBANCO UNIAO DE BANCOS BRASILEIROS S.A. (Unibanco)', + '041': 'BANCO DO ESTADO DO RIO GRANDE DO SUL S.A. (Banrisul)', + '104': 'CAIXA ECONOMICA FEDERAL (Caixa Econômica Federal)', + '399': 'HSBC BANK BRASIL S.A.BANCO MULTIPLO (HSBC)', + '745': 'BANCO CITIBANK S.A.', + '151': 'BANCO NOSSA CAIXA S.A (Nossa Caixa)', + '389': 'BANCO MERCANTIL DO BRASIL S.A. (Mercantil do Brasil)', + '004': 'BANCO DO NORDESTE DO BRASIL S.A (Banco do Nordeste (BNB) )', + '021': 'BANESTES S.A BANCO DO ESTADO DO ESPIRITO SANTO (Banestes)', + '422': 'BANCO SAFRA S.A. (Safra)', + '003': 'BANCO DA AMAZONIA S.A. (Banco da Amazônia (Basa))', + '047': 'Banco do Estado de Sergipe S.A (Banese)', + '070': 'Banco de Brasília S.A. (BRB)', + '655': 'Banco Votorantim S.A (Votorantim)', + '107': 'Banco BBM S.A (BBM)', + '025': 'Banco Alfa S.A (Alfa)', + '263': 'Banco Cacique S. A. (Cacique)', + '229': 'BANCO CRUZEIRO DO SUL S.A. (Cruzeiro do Sul)', + '252': 'BANCO FININVEST S.A. (Fininvest)', + '063': 'BANCO IBI S.A - BANCO MULTIPLO (Banco IBI)', + '623': 'BANCO PANAMERICANO S.A. (PanAmericano)', + '633': 'BANCO RENDIMENTO S.A. (Banco Rendimento)', + '749': 'BANCO SIMPLES S.A. (Banco Simples)', + '215': 'BANCO ACOMERCIAL E DE INVESTIMENTO SUDAMERIS S.A. (Sudameris)', + '756': 'BANCO COOPERATIVO DO BRASIL S.A. - (BANCOOB)', + '748': 'BANCO COOPERATIVO SICREDI S.A. (SICREDI)', + '065': 'LEMON BANK BANCO MÚLTIPLO S..A (Lemon Bank)', + '069': 'BPN BRASIL BANCO MÚLTIPLO S.A. (BPN)', + '719': 'BANIF - BANCO INTERNACIONAL DO FUNCHAL (BRASIL), S.A. (Banif)', + '318': 'BANCO BMG S.A. (BMG)', + '027': 'BANCO DO ESTADO DE SANTA CATARINA S.A.', + '208': 'BANCO UBS PACTUAL S.A.', + '479': 'BANCO ITAUBANK S.A.', + '077': 'BANCO INTERMEDIUM S.A.', + '212': 'BANCO ORIGINAL', + '085': 'CECRED-COOPERATIVA CENTRAL DE CREDITO URBANO' } diff --git a/frontend/src/components/profile/payments.js b/frontend/src/components/profile/payments.js index 313fe5da1..9ed358a72 100644 --- a/frontend/src/components/profile/payments.js +++ b/frontend/src/components/profile/payments.js @@ -1,6 +1,5 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { injectIntl, FormattedMessage } from 'react-intl' +import React, { useEffect, useState } from 'react' + import 'react-placeholder/lib/reactPlaceholder.css' import { messages } from '../task/messages/task-messages' import MomentComponent from 'moment' @@ -8,8 +7,6 @@ import PaymentTypeIcon from '../payment/payment-type-icon' import { Container, - Tabs, - Tab, withStyles, Button, Link, @@ -17,7 +14,6 @@ import { } from '@material-ui/core' import { - Redeem as RedeemIcon, Refresh as RefreshIcon, Cancel as CancelIcon, Info as InfoIcon, @@ -27,6 +23,9 @@ import { import slugify from '@sindresorhus/slugify' import TaskPaymentCancel from '../task/task-payment-cancel' +import PropTypes from 'prop-types' +import { FormattedMessage, injectIntl } from 'react-intl' + import TaskOrderDetails from '../task/order/task-order-details' import TaskOrderTransfer from '../task/order/task-order-transfer' import PaymentRefund from './payment-refund' @@ -46,302 +45,303 @@ const styles = theme => ({ } }) -class Payments extends React.Component { - constructor (props) { - super(props) - - this.state = { - cancelPaypalConfirmDialog: false, - orderDetailsDialog: false, - transferDialogOpen: false, - refundDialogOpen: false, - currentOrderId: null - } +const Payments = ({ classes, tasks, orders, order, user, logged, listOrders, getOrderDetails, cancelPaypalPayment, transferOrder, refundOrder, intl }) => { + const [cancelPaypalConfirmDialog, setCancelPaypalConfirmDialog] = useState(false) + const [orderDetailsDialog, setOrderDetailsDialog] = useState(false) + const [transferDialogOpen, setTransferDialogOpen] = useState(false) + const [refundDialogOpen, setRefundDialogOpen] = useState(false) + const [currentOrderId, setCurrentOrderId] = useState(null) + + const statuses = { + open: intl.formatMessage(messages.openPaymentStatus), + succeeded: intl.formatMessage(messages.succeededStatus), + fail: intl.formatMessage(messages.failStatus), + canceled: intl.formatMessage(messages.canceledStatus), + refunded: intl.formatMessage(messages.refundedStatus) } - async componentDidMount () { - await this.props.listOrders({ userId: this.props.user.id }) - } + useEffect(() => { + listOrders({ userId: user.id }) + }, [listOrders, user.id]) - handlePayPalDialogOpen = (e, id) => { + const handlePayPalDialogOpen = (e, id) => { e.preventDefault() - this.setState({ cancelPaypalConfirmDialog: true, currentOrderId: id }) + setCancelPaypalConfirmDialog(true) + setCurrentOrderId(id) } - handlePayPalDialogClose = () => { - this.setState({ cancelPaypalConfirmDialog: false }) + const handlePayPalDialogClose = () => { + setCancelPaypalConfirmDialog(false) } - handleCancelPaypalPayment = async () => { - const orderId = this.state.currentOrderId - this.setState({ cancelPaypalConfirmDialog: false, orderDetailsDialog: false }) - await this.props.cancelPaypalPayment(orderId) + const handleCancelPaypalPayment = async () => { + const orderId = currentOrderId + setCancelPaypalConfirmDialog(false) + setOrderDetailsDialog(false) + await cancelPaypalPayment(orderId) } - openOrderDetailsDialog = async (e, id) => { - await this.props.getOrderDetails(id) - this.setState({ orderDetailsDialog: true, currentOrderId: id }) + const openOrderDetailsDialog = async (e, id) => { + await getOrderDetails(id) + setOrderDetailsDialog(true) + setCurrentOrderId(id) } - openTransferDialog = async (e, item) => { - await this.props.listTasks({}) - await this.props.filterTasks('userId') - this.setState({ transferDialogOpen: true }) + const openTransferDialog = async (e, item) => { + await listTasks({}) + await filterTasks('userId') + setTransferDialogOpen(true) } - openRefundDialog = async (e, item) => { - this.setState({ refundDialogOpen: true, currentOrderId: item.id }) + const openRefundDialog = async (e, item) => { + setRefundDialogOpen(true) + setCurrentOrderId(item.id) } - closeRefundDialog = async () => { - this.setState({ refundDialogOpen: false }) + const closeRefundDialog = async () => { + setRefundDialogOpen(false) } - closeTransferDialog = (e, item) => { - // await this.props.getOrderDetails(item.id) - this.setState({ transferDialogOpen: false }) + const closeTransferDialog = (e, item) => { + setTransferDialogOpen(false) } - closeOrderDetailsDialog = () => { - this.setState({ orderDetailsDialog: false }) + const closeOrderDetailsDialog = () => { + setOrderDetailsDialog(false) } - render () { - const { classes, orders, user, logged } = this.props + const retryPaypalPayment = (e, paymentUrl) => { + e.preventDefault() - const statuses = { - open: this.props.intl.formatMessage(messages.openPaymentStatus), - succeeded: this.props.intl.formatMessage(messages.succeededStatus), - fail: this.props.intl.formatMessage(messages.failStatus), - canceled: this.props.intl.formatMessage(messages.canceledStatus), - refunded: this.props.intl.formatMessage(messages.refundedStatus) + if (paymentUrl) { + window.location.href = paymentUrl + window.location.reload() } + } - const retryPaypalPayment = (e, paymentUrl) => { - e.preventDefault() + /* + const cancelPaypalPayment = (e, id) => { + e.preventDefault(); - if (paymentUrl) { - window.location.href = paymentUrl - window.location.reload() - } + if (id) { + handlePayPalDialogOpen(e, id); } + }; + */ - const cancelPaypalPayment = (e, id) => { - e.preventDefault() + const retryPaypalPaymentButton = (paymentUrl) => { + return ( + + ) + } - if (id) { - this.handlePayPalDialogOpen(e, id) + const cancelPaypalPaymentButton = (id) => { + return ( + + ) + } + + const detailsOrderButton = (item, userId) => { + if (item.provider === 'paypal') { + if (item.User && userId === item.User.id) { + return ( + + ) } } + } - const retryPaypalPaymentButton = (paymentUrl) => { - return ( - - ) - } + const issueRow = issue => { + return ( + + { issue && issue.title ? ( + { + e.preventDefault() + window.location.href = `/#/task/${issue.id}/${slugify(issue.title)}` + window.location.reload() + } }>{ issue.title } + ) : ( + 'no issue found' + ) } + + ) + } - const cancelPaypalPaymentButton = (id) => { - return ( - - ) + const retryOrCancelButton = (item, userId) => { + if (item.User && item.provider === 'paypal' && userId === item.User.id) { + if ((item.status === 'fail' || item.status === 'open') && item.payment_url) { + return retryPaypalPaymentButton(item.payment_url) + } + else if (item.status === 'succeeded') { + return cancelPaypalPaymentButton(item.id) + } + else { + return '' + } } + } - const detailsOrderButton = (item, userId) => { - if (item.provider === 'paypal') { - if (item.User && userId === item.User.id) { - return ( + const transferButton = (item, userId) => { + if (item.User && item.provider === 'stripe' && userId === item.User.id) { + if (item.status === 'succeeded' && item.Task && item.Task.status === 'open' && item.Task.paid === false && !item.Task.transfer_id) { + return ( + - ) - } + + + ) } - } - - const issueRow = issue => { - return ( - { issue && issue.title - ? ( - { - e.preventDefault() - window.location.href = `/#/task/${issue.id}/${slugify(issue.title)}` - window.location.reload() - } }>{ issue.title } - ) : ( - 'no issue found' - ) - } - ) - } - - const retryOrCancelButton = (item, userId) => { - if (item.User && item.provider === 'paypal' && userId === item.User.id) { - if ((item.status === 'fail' || item.status === 'open') && item.payment_url) { - return retryPaypalPaymentButton(item.payment_url) - } - else if (item.status === 'succeeded') { - return cancelPaypalPaymentButton(item.id) - } - else { - return '' - } + else { + return '' } } + } - const transferButton = (item, userId) => { - if (item.User && item.provider === 'stripe' && userId === item.User.id) { - if (item.status === 'succeeded' && item.Task && item.Task.status === 'open' && item.Task.paid === false && !item.Task.transfer_id) { - return ( - - - - - ) - } - else { - return '' - } + const refundButton = (item, userId) => { + if (item.User && userId === item.User.id) { + if (item.status === 'succeeded' && item.provider === 'stripe' && item.Task && item.Task.status === 'open' && item.Task.paid === false && !item.Task.transfer_id) { + return ( + + + + ) } - } - - const refundButton = (item, userId) => { - if (item.User && userId === item.User.id) { - if (item.status === 'succeeded' && item.provider === 'stripe' && item.Task && item.Task.status === 'open' && item.Task.paid === false && !item.Task.transfer_id) { - return ( - - - - ) - } - else { - return '' - } + else { + return '' } } + } - const displayOrders = orders => { - if (!orders) return [] - - if (!orders.length) { - return [] - } + const displayOrders = orders => { + if (!orders) return [] - let userId + if (!orders.length) { + return [] + } - if (logged) { - userId = user.id - } + let userId - return orders.map((item, i) => [ - item.paid ? this.props.intl.formatMessage(messages.labelYes) : this.props.intl.formatMessage(messages.labelNo), -
- { statuses[item.status] } -
, - issueRow(item.Task), - `$ ${item.amount}`, - , - { MomentComponent(item.createdAt).fromNow() }, -
- { detailsOrderButton(item, userId) } - { retryOrCancelButton(item, userId) } - { transferButton(item, userId) } - { refundButton(item, userId) } -
, - - ]) + if (logged) { + userId = user.id } - return ( -
- - - - -
- -
-
- this.props.listOrders({ userId: this.props.user.id }) } - /> - - this.closeRefundDialog() } - orderId={ this.state.currentOrderId } - onRefund={ this.props.refundOrder } - listOrders={ async () => this.props.listOrders({ userId: this.props.user.id }) } - /> -
- ) + return orders.map((item, i) => [ + item.paid ? intl.formatMessage(messages.labelYes) : intl.formatMessage(messages.labelNo), +
+ { statuses[item.status] } +
, + issueRow(item.Task), + `$ ${item.amount}`, + , + { MomentComponent(item.createdAt).fromNow() }, +
+ { detailsOrderButton(item, userId) } + { retryOrCancelButton(item, userId) } + { transferButton(item, userId) } + { refundButton(item, userId) } +
, + ]) } + + return ( +
+ + + + +
+ +
+
+ listOrders({ userId: user.id }) } + /> + + closeRefundDialog() } + orderId={ currentOrderId } + onRefund={ refundOrder } + listOrders={ async () => listOrders({ userId: user.id }) } + /> +
+ ) } Payments.propTypes = { classes: PropTypes.object.isRequired, handleTabChange: PropTypes.func, user: PropTypes.object, - logged: PropTypes.bool + logged: PropTypes.bool, + listOrders: PropTypes.func, + getOrderDetails: PropTypes.func, + cancelPaypalPayment: PropTypes.func, + transferOrder: PropTypes.func, + refundOrder: PropTypes.func, + intl: PropTypes.object } export default injectIntl(withStyles(styles)(Payments)) diff --git a/frontend/src/components/profile/profile-sidebar.tsx b/frontend/src/components/profile/profile-sidebar.tsx index 5cc6eaa06..ed8ea3f9f 100644 --- a/frontend/src/components/profile/profile-sidebar.tsx +++ b/frontend/src/components/profile/profile-sidebar.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react' import { FormattedMessage } from 'react-intl' import { Grid, Button, MenuList, MenuItem, ListItemIcon, ListItemText } from '@material-ui/core' -import { Home, Business, LibraryBooks, Payment as PaymentIcon } from '@material-ui/icons' +import { Home, Business, LibraryBooks, Payment as PaymentIcon, AccountBalance as TransferIcon } from '@material-ui/icons' import classNames from 'classnames' import logo from '../../images/gitpay-logo.png' import { @@ -143,6 +143,30 @@ const ProfileSidebar = ({ /> } + { userTypes && (userTypes?.includes('contributor') || userTypes?.includes('maintainer')) && + + history.push('/profile/transfers') + } + className={ classes.menuItem } + selected={ selected === 6 } + > + + + + + + + } + /> + + } diff --git a/frontend/src/components/profile/profile.js b/frontend/src/components/profile/profile.js index d40b52ec5..3d393d099 100644 --- a/frontend/src/components/profile/profile.js +++ b/frontend/src/components/profile/profile.js @@ -21,6 +21,7 @@ import PaymentsContainer from '../../containers/payments' import Bottom from '../bottom/bottom' import ProfileOptions from './profile-options' import UserTasksContainer from '../../containers/user-tasks' +import TransfersContainer from '../../containers/transfers' import { UserAccount } from './pages/user-account' import { Page, PageContent } from 'app/styleguide/components/Page' @@ -386,6 +387,14 @@ class Profile extends Component { component={ PaymentsContainer } /> } + { (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')) && + + } ({ + root: { + flexShrink: 0, + color: theme.palette.text.secondary, + marginLeft: theme.spacing(2.5), + }, +}) + +class TablePaginationActions extends React.Component { + handleFirstPageButtonClick = event => { + this.props.onChangePage(event, 0) + }; + + handleBackButtonClick = event => { + this.props.onChangePage(event, this.props.page - 1) + }; + + handleNextButtonClick = event => { + this.props.onChangePage(event, this.props.page + 1) + }; + + handleLastPageButtonClick = event => { + this.props.onChangePage( + event, + Math.max(0, Math.ceil(this.props.count / this.props.rowsPerPage) - 1), + ) + }; + + render () { + const { classes, count, page, rowsPerPage, theme } = this.props + + return ( +
+ this.handleFirstPageButtonClick(e) } + disabled={ page === 0 } + aria-label={ this.props.intl.formatMessage(messages.firstPageLabel) } + > + { theme.direction === 'rtl' ? : } + + this.handleBackButtonClick(e) } + disabled={ page === 0 } + aria-label={ this.props.intl.formatMessage(messages.previousPageLabel) } + > + { theme.direction === 'rtl' ? : } + + this.handleNextButtonClick(e) } + disabled={ page >= Math.ceil(count / rowsPerPage) - 1 } + aria-label={ this.props.intl.formatMessage(messages.nextPageLabel) } + > + { theme.direction === 'rtl' ? : } + + this.handleLastPageButtonClick(e) } + disabled={ page >= Math.ceil(count / rowsPerPage) - 1 } + aria-label={ this.props.intl.formatMessage(messages.lastPageLabel) } + > + { theme.direction === 'rtl' ? : } + +
+ ) + } +} + +TablePaginationActions.propTypes = { + classes: PropTypes.object.isRequired, + count: PropTypes.number.isRequired, + onChangePage: PropTypes.func.isRequired, + page: PropTypes.number.isRequired, + rowsPerPage: PropTypes.number.isRequired, + theme: PropTypes.object.isRequired, +} + +const TablePaginationActionsWrapped = injectIntl(withStyles(actionsStyles, { withTheme: true })( + TablePaginationActions +)) + +const styles = theme => ({ + root: { + width: '100%', + marginTop: theme.spacing(3), + }, + table: { + minWidth: 500 + }, + tableWrapper: { + overflowX: 'auto', + }, +}) + +class CustomPaginationActionsTable extends React.Component { + constructor (props) { + super(props) + + this.state = { + page: 0, + rowsPerPage: 10, + } + } + + handleChangePage = (event, page) => { + this.setState({ page }) + }; + + handleChangeRowsPerPage = event => { + this.setState({ rowsPerPage: event.target.value }) + }; + + handleClickListItem = transfer => { + this.props.history.push(`/transfer/${transfer.id}/${slugify(transfer.title)}`) + } + + goToProject = (e, id, organizationId) => { + e.preventDefault() + window.location.href = '/#/organizations/' + organizationId + '/projects/' + id + window.location.reload() + } + + render () { + const { classes, transfers, tableHead } = this.props + const { rowsPerPage, page } = this.state + const emptyRows = transfers?.data?.length ? rowsPerPage - Math.min(rowsPerPage, transfers.data.length - page * rowsPerPage) : 0 + + return ( + + { transfers.completed && transfers.data.length + ? +
+ + + + { tableHead.map( t => + + {t} + + )} + + + + { transfers.data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map(n => { + return ( + + { n.map( p => + + {p} + + )} + + ) + }) } + { emptyRows > 0 && ( + + + + ) } + + + + this.handleChangePage(e, page) } + onChangeRowsPerPage={ (e, page) => this.handleChangeRowsPerPage(e, page) } + Actions={ TablePaginationActionsWrapped } + /> + + +
+
+
+ :
+ + + +
} +
+ ) + } +} + +CustomPaginationActionsTable.propTypes = { + classes: PropTypes.object.isRequired, + history: PropTypes.object, + payments: PropTypes.object +} + +export default injectIntl(withRouter(withStyles(styles)(CustomPaginationActionsTable))) diff --git a/frontend/src/components/profile/transfers.js b/frontend/src/components/profile/transfers.js new file mode 100644 index 000000000..c2f348d14 --- /dev/null +++ b/frontend/src/components/profile/transfers.js @@ -0,0 +1,105 @@ +import React, { useEffect } from 'react' +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl' +import slugify from '@sindresorhus/slugify' +import moment from 'moment' +import { + Container, + Typography, + withStyles, + Chip, + Tabs, + Tab +} from '@material-ui/core' +import { messages } from '../task/messages/task-messages' +import CustomPaginationActionsTable from './transfer-table' + +const transferMessages = defineMessages({ + cardTableHeaderFrom: { + id: 'card.table.header.from', + defaultMessage: 'Transfers sent' + }, + cardTableHeaderTo: { + id: 'card.table.header.to', + defaultMessage: 'Transfers received' + }, +}) + +const styles = theme => ({ + paper: { + padding: 10, + marginTop: 10, + marginBottom: 10, + textAlign: 'left', + color: theme.palette.text.secondary + }, + button: { + width: 100, + font: 10 + } +}) + +const Transfers = ({ searchTransfer, transfers, user, intl }) => { + const [value, setValue] = React.useState(0) + + const handleChange = (event, newValue) => { + setValue(newValue) + let getTransfers = () => {} + if (newValue === 'to') { + getTransfers = async () => await searchTransfer({ to: user.user.id }) + } + if (newValue === 'from') { + getTransfers = async () => await searchTransfer({ userId: user.user.id }) + } + getTransfers().then(t => console.log('transfers:', t)) + } + + useEffect(() => { + setValue('from') + const getTranfers = async () => await searchTransfer({ userId: user.user.id }) + getTranfers().then(t => console.log('transfers:', t)) + }, [user]) + + return ( +
+ + + + + + + + +
+ [ + , + `$ ${t.value}`, + moment(t.createdAt).format('LLL'), + + { t.Task.title } + + ]) } || {} + } + /> +
+
+
+ ) +} + +export default injectIntl(withStyles(styles)(Transfers)) diff --git a/frontend/src/components/task/messages/task-messages.js b/frontend/src/components/task/messages/task-messages.js index 5525c730a..3b2ca0899 100644 --- a/frontend/src/components/task/messages/task-messages.js +++ b/frontend/src/components/task/messages/task-messages.js @@ -102,6 +102,10 @@ export const messages = defineMessages({ id: 'task.card.table.header.actions', defaultMessage: 'Actions' }, + cardTableHeaderIssue: { + id: 'task.card.table.header.issue', + defaultMessage: 'Issue' + }, cardTableHeaderValue: { id: 'task.card.table.header.value', defaultMessage: 'Value' diff --git a/frontend/src/components/task/task-payment.js b/frontend/src/components/task/task-payment.js index 9f0a43e1e..03d58ccb9 100644 --- a/frontend/src/components/task/task-payment.js +++ b/frontend/src/components/task/task-payment.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import MomentComponent from 'moment' import { injectIntl, defineMessages, FormattedMessage } from 'react-intl' import Alert from '@material-ui/lab/Alert' +import AlertTitle from '@material-ui/lab/AlertTitle' import { withStyles, Button, @@ -26,6 +27,7 @@ import { PaymentOutlined as FilterListIcon, Redeem as RedeemIcon } from '@material-ui/icons' +import { Link } from 'react-router-dom' import blue from '@material-ui/core/colors/blue' import PaymentTypeIcon from '../payment/payment-type-icon' import InterestedUsers from './components/interested-users' @@ -133,7 +135,8 @@ class TaskPayment extends Component { } payTask = e => { - this.props.onPayTask(this.props.id, this.props.values.card) + //this.props.onPayTask(this.props.id, this.props.values.card) + this.props.onTransferTask(this.props.id) this.props.onClose() } @@ -281,15 +284,37 @@ class TaskPayment extends Component { - { this.props.transferId ? ( -
- - - - - { `${this.props.transferId}` } - -
+ { (this.props.transferId || this.props.task.Transfer) ? ( + + + } + > + + + + + + + {this.props.transferId ? + + { `${this.props.transferId}` } + + : +
+ + + + +
+ } +
) : { orders.length > 0 ? orders.map((order, index) => ( @@ -308,18 +333,7 @@ class TaskPayment extends Component { /> { !order.transfer_id ? ( - + ) : ( } secondary={ `${this.statuses(order.status) + ' ' + MomentComponent(order.createdAt).fromNow() || this.props.intl.formatMessage(messages.labelCreditCard)}` } /> - + ) } @@ -378,7 +381,7 @@ class TaskPayment extends Component {
- { !this.props.paid ? ( + { (!this.props.paid || this.props.task.Transfer.id) ? (
{ this.props.assigned @@ -455,7 +458,7 @@ class TaskPayment extends Component { style={ { float: 'right', margin: 10 } } variant='contained' color='primary' - disabled={ !this.props.assigned || this.props.transferId} + disabled={ !this.props.assigned || this.props.transferId || this.props.task.Transfer} > { - + @@ -83,7 +83,7 @@ const Welcome = (props) => { - + @@ -99,7 +99,7 @@ const Welcome = (props) => { - + @@ -137,7 +137,7 @@ const Welcome = (props) => { - + @@ -152,7 +152,7 @@ const Welcome = (props) => { /> - + @@ -167,7 +167,7 @@ const Welcome = (props) => { /> - + @@ -205,7 +205,7 @@ const Welcome = (props) => { - + @@ -220,7 +220,7 @@ const Welcome = (props) => { /> - + @@ -235,7 +235,7 @@ const Welcome = (props) => { /> - + diff --git a/frontend/src/containers/task.js b/frontend/src/containers/task.js index 81f85525c..a4260f62e 100644 --- a/frontend/src/containers/task.js +++ b/frontend/src/containers/task.js @@ -3,7 +3,7 @@ import Task from '../components/task/task' import { addNotification, addDialog, closeDialog } from '../actions/notificationActions' import { loggedIn } from '../actions/loginActions' import { assignTask, removeAssignment, messageTask, messageOffer, offerUpdate, actionAssign } from '../actions/assignActions' -import { listTasks, filterTasks, updateTask, deleteTask, fetchTask, paymentTask, syncTask, changeTaskTab, filterTaskOrders, inviteTask, fundingInviteTask, messageAuthor, reportTask, requestClaimTask } from '../actions/taskActions' +import { listTasks, filterTasks, updateTask, deleteTask, fetchTask, paymentTask, syncTask, changeTaskTab, filterTaskOrders, inviteTask, fundingInviteTask, messageAuthor, reportTask, requestClaimTask, transferTask } from '../actions/taskActions' import { createOrder, payOrder, transferOrder, cancelOrder, detailOrder, listOrders } from '../actions/orderActions' import { getTaskOrdersByFilter } from '../selectors/task' import { getFilteredTasks, getProject } from '../selectors/tasks' @@ -42,6 +42,7 @@ const mapDispatchToProps = (dispatch, ownProps) => { fetchTask: (taskId) => dispatch(fetchTask(taskId)), syncTask: (taskId) => dispatch(syncTask(taskId)), paymentTask: (taskId, value) => dispatch(paymentTask(taskId, value)), + transferTask: (taskId) => dispatch(transferTask(taskId)), paymentOrder: (order) => dispatch(payOrder(order)), changeTab: (tab) => dispatch(changeTaskTab(tab)), createOrder: (order) => dispatch(createOrder(order)), diff --git a/frontend/src/containers/transfers.ts b/frontend/src/containers/transfers.ts new file mode 100644 index 000000000..5a7a57346 --- /dev/null +++ b/frontend/src/containers/transfers.ts @@ -0,0 +1,18 @@ +import { connect } from 'react-redux'; +import { searchTransfer } from '../actions/transferActions'; +import Transfers from '../components/profile/transfers'; + +const mapStateToProps = (state: any) => { + return { + user: state.loggedIn, + transfers: state.transfers + } +} + +const mapDispatchToProps = (dispatch: any) => { + return { + searchTransfer: (params: any) => dispatch(searchTransfer(params)) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Transfers); \ No newline at end of file diff --git a/frontend/src/reducers/reducers.js b/frontend/src/reducers/reducers.js index cfd530763..c08377031 100644 --- a/frontend/src/reducers/reducers.js +++ b/frontend/src/reducers/reducers.js @@ -17,6 +17,7 @@ import taskSolution from './taskSolutionReducer' import couponReducer from './couponReducer' import { profileReducer } from './profileReducer' import { labels } from './labelReducer' +import { transfers } from './transfersReducer' const reducers = combineReducers({ notification, @@ -41,7 +42,8 @@ const reducers = combineReducers({ taskSolutionReducer: taskSolution, couponReducer: couponReducer, profileReducer: profileReducer, - intl: intlReducer + intl: intlReducer, + transfers }) export default reducers diff --git a/frontend/src/reducers/taskReducer.js b/frontend/src/reducers/taskReducer.js index b23ffc908..f519bd242 100644 --- a/frontend/src/reducers/taskReducer.js +++ b/frontend/src/reducers/taskReducer.js @@ -74,6 +74,7 @@ export const task = (state = { orders: [], assigns: [], assignedUser: {}, + Transfer: {}, url: '', provider: null, metadata: { diff --git a/frontend/src/reducers/transfersReducer.js b/frontend/src/reducers/transfersReducer.js new file mode 100644 index 000000000..c3322e6fa --- /dev/null +++ b/frontend/src/reducers/transfersReducer.js @@ -0,0 +1,18 @@ +import { + SEARCH_TRANSFER_REQUESTED, + SEARCH_TRANSFER_SUCCESS, + SEARCH_TRANSFER_FAILED +} from '../actions/transferActions' + +export const transfers = (state = { data: [], completed: false }, action) => { + switch (action.type) { + case SEARCH_TRANSFER_REQUESTED: + return { ...state, completed: action.completed } + case SEARCH_TRANSFER_SUCCESS: + return { ...state, completed: action.completed, data: action.data } + case SEARCH_TRANSFER_FAILED: + return { ...state, error: action.error, completed: action.completed } + default: + return state + } +} diff --git a/migration/migrations/20240102224715-create-transfer.js b/migration/migrations/20240102224715-create-transfer.js new file mode 100644 index 000000000..41c7ce29b --- /dev/null +++ b/migration/migrations/20240102224715-create-transfer.js @@ -0,0 +1,45 @@ +'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.createTable('Transfers', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + status: Sequelize.STRING, + value: Sequelize.DECIMAL, + transfer_id: Sequelize.STRING, + transfer_method: Sequelize.STRING, + taskId: { + type: Sequelize.INTEGER, + references: { + model: 'Tasks', + key: 'id' + }, + allowNull: false, + }, + createdAt: Sequelize.DATE, + updatedAt: Sequelize.DATE + }); + }, + + async down (queryInterface, Sequelize) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + await queryInterface.dropTable('transfers'); + } +}; diff --git a/migration/migrations/20240105133559-add-transferId-to-task.js b/migration/migrations/20240105133559-add-transferId-to-task.js new file mode 100644 index 000000000..a2d37d12c --- /dev/null +++ b/migration/migrations/20240105133559-add-transferId-to-task.js @@ -0,0 +1,38 @@ +'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( + 'Tasks', + 'TransferId', + { + type: Sequelize.INTEGER, + references: { + model: 'Transfers', + key: 'id' + }, + allowNull: true + } + ); + }, + + async down (queryInterface, Sequelize) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + queryInterface.removeColumn( + 'Tasks', + 'TransferId' + ); + } +}; diff --git a/migration/migrations/20240111183036-add-userId-to-transfer.js b/migration/migrations/20240111183036-add-userId-to-transfer.js new file mode 100644 index 000000000..5b5b39c6d --- /dev/null +++ b/migration/migrations/20240111183036-add-userId-to-transfer.js @@ -0,0 +1,38 @@ +'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', + 'userId', + { + type: Sequelize.INTEGER, + references: { + model: 'Users', + key: 'id' + }, + allowNull: false + } + ); + }, + + async down (queryInterface, Sequelize) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + queryInterface.removeColumn( + 'Transfers', + 'userId' + ); + } +}; diff --git a/migration/migrations/20240111183241-add-to-to-transfer.js b/migration/migrations/20240111183241-add-to-to-transfer.js new file mode 100644 index 000000000..b321400f9 --- /dev/null +++ b/migration/migrations/20240111183241-add-to-to-transfer.js @@ -0,0 +1,38 @@ +'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', + 'to', + { + type: Sequelize.INTEGER, + references: { + model: 'Users', + key: 'id' + }, + allowNull: true + } + ); + }, + + async down (queryInterface, Sequelize) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + queryInterface.removeColumn( + 'Transfers', + 'to' + ); + } +}; diff --git a/models/task.js b/models/task.js index f75104fd2..2fa0f1577 100644 --- a/models/task.js +++ b/models/task.js @@ -33,6 +33,14 @@ module.exports = (sequelize, DataTypes) => { }, allowNull: true, }, + TransferId: { + type: DataTypes.INTEGER, + references: { + model: 'Transfers', + key: 'id' + }, + allowNull: true, + }, ProjectId: { type: DataTypes.INTEGER, references: { @@ -102,6 +110,7 @@ module.exports = (sequelize, DataTypes) => { } ) Task.hasMany(models.TaskSolution, { foreignKey: 'taskId' }) + Task.hasOne(models.Transfer, { foreignKey: 'taskId' }) } return Task diff --git a/models/transfer.js b/models/transfer.js new file mode 100644 index 000000000..e3bde4703 --- /dev/null +++ b/models/transfer.js @@ -0,0 +1,48 @@ +module.exports = (sequelize, DataTypes) => { + const Transfer = sequelize.define('Transfer', { + status: { + type: DataTypes.STRING, + defaultValue: 'pending' + }, + value: DataTypes.DECIMAL, + transfer_id: DataTypes.STRING, + transfer_method: DataTypes.STRING, + taskId: { + type: DataTypes.INTEGER, + references: { + model: 'Tasks', + key: 'id' + }, + allowNull: false, + }, + userId: { + type: DataTypes.INTEGER, + references: { + model: 'Users', + key: 'id' + }, + allowNull: false, + }, + to: { + type: DataTypes.INTEGER, + references: { + model: 'Users', + key: 'id' + }, + allowNull: false, + } + }, { + + }) + + Transfer.associate = function (models) { + Transfer.belongsTo(models.Task, { + foreignKey: 'taskId' + }) + Transfer.belongsTo(models.User, { + foreignKey: 'userId' + }) + } + + return Transfer +} diff --git a/modules/app/controllers/transfer.js b/modules/app/controllers/transfer.js new file mode 100644 index 000000000..749651a06 --- /dev/null +++ b/modules/app/controllers/transfer.js @@ -0,0 +1,20 @@ +const Transfer = require('../../transfers') +exports.createTransfer = (req, res) => { + Transfer.transferBuilds(req.body) + .then(data => { + res.send(data) + }).catch(error => { + res.status(error.StatusCodeError || 400).send(error) + }) +} + +exports.searchTransfer = (req, res) => { + Transfer.transferSearch(req.query) + .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/controllers/webhook.js b/modules/app/controllers/webhook.js index 9be771186..c4c027b97 100644 --- a/modules/app/controllers/webhook.js +++ b/modules/app/controllers/webhook.js @@ -6,7 +6,6 @@ if (process.env.NODE_ENV !== 'production') { const i18n = require('i18n') const dateFormat = require('dateformat') const moment = require('moment') - const models = require('../../../models') const constants = require('../../mail/constants') const TaskMail = require('../../mail/task') @@ -241,7 +240,6 @@ exports.github = async (req, res) => { } })) const allResponse = { ...response, totalLabelResponse } - console.log(allResponse, 'response after executing labls') return res.json({ ...allResponse }) } catch (e) { @@ -260,7 +258,6 @@ exports.github = async (req, res) => { exports.updateWebhook = (req, res) => { // eslint-disable-next-line no-console - console.log('webhook body', req.body) if (req.body.object === 'event') { const event = req.body @@ -657,32 +654,48 @@ exports.updateWebhook = (req, res) => { }) break case 'payout.paid': - return models.User.findOne({ - where: { - account_id: event.account - } - }) - .then(user => { - if (user) { - const date = new Date(event.data.object.arrival_date * 1000) - const language = user.language || 'en' - i18n.setLocale(language) - SendMail.success( - user.dataValues, - i18n.__('mail.webhook.payment.transfer.finished.subject'), - i18n.__('mail.webhook.payment.transfer.finished.message', { - currency: CURRENCIES[event.data.object.currency], - amount: event.data.object.amount / 100, - date: date + return stripe.balanceTransactions + .retrieve(event.data.object.balance_transaction).then((balance_transaction) => { + return models.Transfer.update({ + status: event.data.object.status + }, { + where: { + transfer_id: balance_transaction.source + } + }).then(updateTransfer => { + return models.User.findOne({ + where: { + account_id: event.account + } + }) + .then(user => { + if (user) { + const date = new Date(event.data.object.arrival_date * 1000) + const language = user.language || 'en' + i18n.setLocale(language) + SendMail.success( + user.dataValues, + i18n.__('mail.webhook.payment.transfer.finished.subject'), + i18n.__('mail.webhook.payment.transfer.finished.message', { + currency: CURRENCIES[event.data.object.currency], + amount: event.data.object.amount / 100, + date: date + }) + ) + return res.json(req.body) + } }) - ) - return res.json(req.body) - } + .catch(e => { + console.log('error to find user', e) + return res.status(400).send(e) + }) + }) }) .catch(e => { + console.log('error to find balance transaction', e) return res.status(400).send(e) - }) - + } + ) break case 'balance.available': SendMail.success( diff --git a/modules/app/index.js b/modules/app/index.js index a87228294..ea5f1d9b5 100644 --- a/modules/app/index.js +++ b/modules/app/index.js @@ -14,6 +14,7 @@ const routerTaskSolution = require('./routes/taskSolutions') const routerCoupon = require('./routes/coupon') const routerLabel = require('./routes/label') const routerOffer = require('./routes/offer') +const routerTransfer = require('./routes/transfer') exports.init = (app) => { app.use('/', routerAuth) @@ -30,4 +31,5 @@ exports.init = (app) => { app.use('/coupon', routerCoupon) app.use('/labels', routerLabel) app.use('/offers', routerOffer) + app.use('/transfers', routerTransfer) } diff --git a/modules/app/routes/transfer.js b/modules/app/routes/transfer.js new file mode 100644 index 000000000..5aab01169 --- /dev/null +++ b/modules/app/routes/transfer.js @@ -0,0 +1,9 @@ +const express = require('express') +const router = express.Router() +require('../../authenticationHelpers') +const controllers = require('../controllers/transfer') + +router.post('/create', controllers.createTransfer) +router.get('/search', controllers.searchTransfer) + +module.exports = router diff --git a/modules/tasks/taskFetch.js b/modules/tasks/taskFetch.js index 07ef2981c..587151a33 100644 --- a/modules/tasks/taskFetch.js +++ b/modules/tasks/taskFetch.js @@ -39,6 +39,9 @@ module.exports = Promise.method(function taskFetch (taskParams) { }, { model: models.Label + }, + { + model: models.Transfer } ] @@ -141,6 +144,7 @@ module.exports = Promise.method(function taskFetch (taskParams) { issue: issueDataJsonGithub }, orders: data.dataValues.Orders, + Transfer: data.dataValues.Transfer, Assigns: data.dataValues.Assigns, members: data.dataValues.Members, Offers: data.dataValues.Offers, @@ -238,6 +242,7 @@ module.exports = Promise.method(function taskFetch (taskParams) { } }, orders: data.dataValues.Orders, + Transfer: data.dataValues.Transfer, assigns: data.dataValues.Assigns, members: data.dataValues.Members, Offers: data.dataValues.Offers diff --git a/modules/transfers/index.js b/modules/transfers/index.js new file mode 100644 index 000000000..753b38d32 --- /dev/null +++ b/modules/transfers/index.js @@ -0,0 +1,7 @@ +const transferBuilds = require('./transferBuilds') +const transferSearch = require('./transferSearch') + +module.exports = { + transferBuilds, + transferSearch +} diff --git a/modules/transfers/transferBuilds.js b/modules/transfers/transferBuilds.js new file mode 100644 index 000000000..cd01e8db2 --- /dev/null +++ b/modules/transfers/transferBuilds.js @@ -0,0 +1,153 @@ +const Transfer = require('../../models').Transfer +const Task = require('../../models').Task +const Order = require('../../models').Order +const Promise = require('bluebird') +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') + +module.exports = Promise.method(async function transferBuilds (params) { + const existingTransfer = params.transfer_id && await Transfer.findOne({ + where: { + transfer_id: params.transfer_id + } + }) + + if (existingTransfer) { + return { error: 'This transfer already exists' } + } + + const existingTask = params.taskId && await Transfer.findOne({ + where: { + taskId: params.taskId + } + }) + + if (existingTask) { + return { error: 'Only one transfer for an issue' } + } + + const task = params.taskId && await Task.findOne({ + where: { + id: params.taskId + }, + include: [Order, models.User] + }) + + const taskData = task.dataValues + + if (!taskData) return { error: 'No valid task' } + + if (!taskData.assigned) { + return { error: 'No user assigned' } + } + + const assign = await models.Assign.findOne({ + where: { + id: taskData.assigned + }, + include: [ models.User ] + }) + + let finalValue = 0 + let isStripe = false + let isPaypal = false + let isMultiple = false + + let allStripe = true + let allPaypal = true + + if (!taskData) { + return new Error('Task not found') + } + if (taskData.Orders.length === 0) { + return { error: 'No orders found' } + } + else { + const orders = taskData.Orders + const ordersPaid = orders.find(order => order.paid === true) + if (!ordersPaid) { + return { error: 'All orders must be paid' } + } + orders.map(order => { + if (order.provider === 'stripe') { + allPaypal = false + isStripe = true + } + if (order.provider === 'paypal') { + allStripe = false + isPaypal = true + } + finalValue += order.amount + }) + if (isStripe && isPaypal) { + isMultiple = true + } + } + const 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, + }).save() + const taskUpdate = await Task.update({ TransferId: transfer.id }, { + where: { + id: params.taskId + } + }) + if (!taskUpdate[0]) { + return { error: 'Task not updated' } + } + + if (allStripe) { + 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', + } + + 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: 'in_transit' }, { + where: { + id: transfer.id + }, + returning: true + + }) + if (!updateTask || !updateTransfer) { + TransferMail.error(user, task, task.value) + return { error: 'update_task_reject' } + } + 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 + } + } + return transfer +}) diff --git a/modules/transfers/transferSearch.js b/modules/transfers/transferSearch.js new file mode 100644 index 000000000..dc17998e8 --- /dev/null +++ b/modules/transfers/transferSearch.js @@ -0,0 +1,22 @@ +const Promise = require('bluebird') +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 transferSearch (params = {}) { + let transfers = [] + if (params.userId) { + transfers = await Transfer.findAll({ + where: { userId: params.userId }, + include: [ Task, User ] + }) + } + if (params.to) { + transfers = await Transfer.findAll({ + where: { to: params.to }, + include: [ Task, User ] + }) + } + return transfers +}) diff --git a/package-lock.json b/package-lock.json index e8ffc1b6f..1db98b35c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,7 @@ "sequelize": "^6.1.0", "sequelize-cli": "^6.6.0", "sequelize-heroku": "^1.0.0", - "stripe": "^5.8.0", + "stripe": "^14.12.0", "umzug": "^1.12.0", "url": "^0.11.0", "url-search-params": "^1.0.2", @@ -663,14 +663,18 @@ } }, "node_modules/ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dependencies": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ajv-errors": { @@ -4014,6 +4018,17 @@ "node": ">=4" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==", + "dependencies": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, "node_modules/eslint/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -4022,6 +4037,16 @@ "ms": "^2.1.1" } }, + "node_modules/eslint/node_modules/fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==" + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==" + }, "node_modules/eslint/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4562,9 +4587,9 @@ ] }, "node_modules/fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", @@ -5335,31 +5360,6 @@ "node": ">=6" } }, - "node_modules/har-validator/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/har-validator/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/har-validator/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -6951,9 +6951,9 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "node_modules/json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -11901,31 +11901,6 @@ "node": ">= 4" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, "node_modules/scss-tokenizer": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz", @@ -13577,26 +13552,30 @@ } }, "node_modules/stripe": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-5.10.0.tgz", - "integrity": "sha512-AUDmXfNAAY/oOfW87HPO4bDzNWJp8iQd0blVWwwEgPxO1DmEC//foI0C9rhr2ZNsuF6kLypPfNtGB9Uf+RCQzQ==", + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.12.0.tgz", + "integrity": "sha512-3lze4QdO8fM6nh1vaRsnFpaoA0WV7DerYtjEprOwcwyfdF5LdesfQNZiT22LO1dFiYobBifDzEn8V51MXHPrPQ==", "dependencies": { - "lodash.isplainobject": "^4.0.6", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1" + "@types/node": ">=8.1.0", + "qs": "^6.11.0" }, "engines": { - "node": ">=4" + "node": ">=12.*" } }, - "node_modules/stripe/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" + "node_modules/stripe/node_modules/@types/node": { + "version": "20.11.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.4.tgz", + "integrity": "sha512-6I0fMH8Aoy2lOejL3s4LhyIYX34DPwY8bl5xlNjBvUEk8OHrcuzsFt+Ied4LvJihbtXPM+8zUqdydfIti86v9g==", + "dependencies": { + "undici-types": "~5.26.4" } }, + "node_modules/stripe/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/superagent": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", @@ -13697,6 +13676,17 @@ "string-width": "^2.1.1" } }, + "node_modules/table/node_modules/ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==", + "dependencies": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, "node_modules/table/node_modules/ajv-keywords": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", @@ -13705,6 +13695,16 @@ "ajv": "^5.0.0" } }, + "node_modules/table/node_modules/fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==" + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==" + }, "node_modules/tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", @@ -15988,21 +15988,6 @@ "node": ">=0.4.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/webpack/node_modules/eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -16023,16 +16008,6 @@ "node": ">=4.0" } }, - "node_modules/webpack/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -17028,25 +17003,27 @@ } }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "ajv-errors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "requires": {} }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "requires": {} }, "ansi-colors": { "version": "4.1.1", @@ -18170,7 +18147,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/chai-spies/-/chai-spies-1.0.0.tgz", "integrity": "sha512-elF2ZUczBsFoP07qCfMO/zeggs8pqCf3fZGyK5+2X4AndS8jycZYID91ztD9oQ7d/0tnS963dPkd0frQEThDsg==", - "dev": true + "dev": true, + "requires": {} }, "chalk": { "version": "2.4.2", @@ -19538,6 +19516,17 @@ "text-table": "~0.2.0" }, "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, "debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -19546,6 +19535,16 @@ "ms": "^2.1.1" } }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -19561,12 +19560,14 @@ "eslint-config-standard": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-11.0.0.tgz", - "integrity": "sha512-oDdENzpViEe5fwuRCWla7AXQd++/oyIp8zP+iP9jiUPG6NBj3SHgdgtl/kTn00AjeN+1HNvavTKmYbMo+xMOlw==" + "integrity": "sha512-oDdENzpViEe5fwuRCWla7AXQd++/oyIp8zP+iP9jiUPG6NBj3SHgdgtl/kTn00AjeN+1HNvavTKmYbMo+xMOlw==", + "requires": {} }, "eslint-config-standard-jsx": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-5.0.0.tgz", - "integrity": "sha512-rLToPAEqLMPBfWnYTu6xRhm2OWziS2n40QFqJ8jAM8NSVzeVKTa3nclhsU4DpPJQRY60F34Oo1wi/71PN/eITg==" + "integrity": "sha512-rLToPAEqLMPBfWnYTu6xRhm2OWziS2n40QFqJ8jAM8NSVzeVKTa3nclhsU4DpPJQRY60F34Oo1wi/71PN/eITg==", + "requires": {} }, "eslint-config-standard-react": { "version": "6.0.0", @@ -19751,7 +19752,8 @@ "eslint-plugin-standard": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-3.1.0.tgz", - "integrity": "sha512-fVcdyuKRr0EZ4fjWl3c+gp1BANFJD1+RaWa2UPYfMZ6jCtp5RG00kSaXnK/dE5sYzt4kaWJ9qdxqUfc0d9kX0w==" + "integrity": "sha512-fVcdyuKRr0EZ4fjWl3c+gp1BANFJD1+RaWa2UPYfMZ6jCtp5RG00kSaXnK/dE5sYzt4kaWJ9qdxqUfc0d9kX0w==", + "requires": {} }, "eslint-scope": { "version": "3.7.3", @@ -20176,9 +20178,9 @@ "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { "version": "2.1.0", @@ -20753,29 +20755,6 @@ "requires": { "ajv": "^6.12.3", "har-schema": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - } } }, "hard-rejection": { @@ -21985,9 +21964,9 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -22168,7 +22147,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz", "integrity": "sha512-SENGE9DhlIIFTSZWiNq4eGeXL8G6z9cqHIOdkx9jh1qhhQqwEy3tAoLRyER0vOcHqdOlKmGpOuXk+HOipIy7sg==", - "dev": true + "dev": true, + "requires": {} }, "karma-jasmine-html-reporter": { "version": "0.2.2", @@ -24593,7 +24573,8 @@ "pg-pool": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", - "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==" + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "requires": {} }, "pg-protocol": { "version": "1.6.0", @@ -25835,29 +25816,6 @@ "ajv": "^6.1.0", "ajv-errors": "^1.0.0", "ajv-keywords": "^3.1.0" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - } } }, "scss-tokenizer": { @@ -27177,19 +27135,26 @@ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" }, "stripe": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-5.10.0.tgz", - "integrity": "sha512-AUDmXfNAAY/oOfW87HPO4bDzNWJp8iQd0blVWwwEgPxO1DmEC//foI0C9rhr2ZNsuF6kLypPfNtGB9Uf+RCQzQ==", + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.12.0.tgz", + "integrity": "sha512-3lze4QdO8fM6nh1vaRsnFpaoA0WV7DerYtjEprOwcwyfdF5LdesfQNZiT22LO1dFiYobBifDzEn8V51MXHPrPQ==", "requires": { - "lodash.isplainobject": "^4.0.6", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1" + "@types/node": ">=8.1.0", + "qs": "^6.11.0" }, "dependencies": { - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + "@types/node": { + "version": "20.11.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.4.tgz", + "integrity": "sha512-6I0fMH8Aoy2lOejL3s4LhyIYX34DPwY8bl5xlNjBvUEk8OHrcuzsFt+Ied4LvJihbtXPM+8zUqdydfIti86v9g==", + "requires": { + "undici-types": "~5.26.4" + } + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" } } }, @@ -27270,10 +27235,32 @@ "string-width": "^2.1.1" }, "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, "ajv-keywords": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha512-ZFztHzVRdGLAzJmpUT9LNFLe1YiVOEylcaNpEutM26PVTCtOD919IMfD01CgbRouB42Dd9atjx1HseC15DgOZA==" + "integrity": "sha512-ZFztHzVRdGLAzJmpUT9LNFLe1YiVOEylcaNpEutM26PVTCtOD919IMfD01CgbRouB42Dd9atjx1HseC15DgOZA==", + "requires": {} + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==" } } }, @@ -28538,17 +28525,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==" }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -28562,16 +28538,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" } } }, @@ -29321,7 +29287,8 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true + "dev": true, + "requires": {} }, "x-frame-options": { "version": "1.0.0", diff --git a/package.json b/package.json index bf3b2786d..bd6e62d6f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "migrate-test-rollback": "NODE_ENV=test node migrate.js prev", "report": "NODE_ENV=production node report.js", "rollback": "node migrate.js prev", + "rollback-test": "NODE_ENV=test node migrate.js prev", "reset": "node migrate.js reset-hard", "start:dev": "nodemon ./server.js --ignore '/frontend/*'", "start": "node ./server.js", @@ -86,7 +87,7 @@ "sequelize": "^6.1.0", "sequelize-cli": "^6.6.0", "sequelize-heroku": "^1.0.0", - "stripe": "^5.8.0", + "stripe": "^14.12.0", "umzug": "^1.12.0", "url": "^0.11.0", "url-search-params": "^1.0.2", diff --git a/test/data/balance.transaction.js b/test/data/balance.transaction.js new file mode 100644 index 000000000..a79044cc5 --- /dev/null +++ b/test/data/balance.transaction.js @@ -0,0 +1,17 @@ +module.exports.get = { + "id": "txn_1CdprOLlCJ9CeQRe7gBPy9Lo", + "object": "balance_transaction", + "amount": -400, + "available_on": 1678043844, + "created": 1678043844, + "currency": "usd", + "description": null, + "exchange_rate": null, + "fee": 0, + "fee_details": [], + "net": -400, + "reporting_category": "transfer", + "source": "tr_1CZ5vkLlCJ9CeQRe", + "status": "available", + "type": "transfer" +} \ No newline at end of file diff --git a/test/data/transfer.js b/test/data/transfer.js index be9b0f2a6..83795405d 100644 --- a/test/data/transfer.js +++ b/test/data/transfer.js @@ -1,4 +1,4 @@ -module.exports.update = { +module.exports.transfer = { "id": "evt_1CcecMBrSjgsps2DMFZw5Tyx", "object": "event", "api_version": "2018-02-28", diff --git a/test/helpers/index.js b/test/helpers/index.js index 12b7c8db0..0174a4f81 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -1,3 +1,5 @@ +const { create } = require('core-js/core/object') +const models = require('../../models') const testEmail = `teste+${Math.random()*100}@gmail.com` const testPassword = 'test' const testName = 'Test' @@ -26,13 +28,13 @@ const activate = (agent, res) => { .get(`/auth/activate?token=${res.body.activation_token}&userId=${res.body.id}`) } -const registerAndLogin = (agent) => { - return register(agent) +const registerAndLogin = (agent, params = {}) => { + return register(agent, params = {}) .then((a) => { return activate(agent, a).then((active) => { - return login(agent).then( + return login(agent, params).then( (res) => { - return res + return a } ).catch((e) => { console.log('error on login', e) @@ -46,17 +48,100 @@ const registerAndLogin = (agent) => { } const createTask = (agent, params = {}) => { + params.provider = params.provider || 'github' - params.url = params.url || 'https://github.com/worknenjoy/truppie/issues/120' - registerAndLogin(agent).then((res) => { - return agent - .post('/tasks') - .send(params) + params.url = params.url || 'https://github.com/worknenjoy/gitpay/issues/221' + + return registerAndLogin(agent).then((res) => { + const user = res.body + return models.Task.create({ + provider: params.provider || 'github', + url: params.url, + userId: user.id + }, { + include: [ models.User ] + + }).then(task => { + return task + }).catch((e) => { + console.log('error on createTask', e) + }) + }).catch((e) => { + console.log('error on registerAndLogin', e) + }) +} + +const createAssign = (agent, params = {}) => { + return register(agent,{ + email: `${Math.random()}anotheruser@example.com`, + password: '123345', + confirmPassword: '123345', + name: 'Foo Bar', + account_id: 'acct_1Gqj2tGjYvP2Yx5R' + }).then((res) => { + const user = res.body + return models.Assign.create({ + TaskId: params.taskId, + userId: user.id + }, { + include: [ models.User ] + }).then((assigned) => { + const assignedData = assigned.dataValues + return models.Task.update({assigned: assignedData.id}, {where: {id: assignedData.TaskId}}).then( task => { + return assigned + }).catch((e) => { + console.log('error on updateTask', e) + }) + }).catch((e) => { + console.log('error on createAssign', e) + }) + }).catch((e) => { + console.log('error on registerAndLogin', e) }) } +const createOrder = (params = {}) => { + + params.source_id = params.source_id || '1234' + params.status = params.status || 'open' + params.amount = 200 + + return models.Order.create(params).then(order => { + return order + }).catch((e) => { + console.log('error on createTask', e) + }) +} + +const createTransfer = (params = {}) => { + + params.transfer_id = params.transfer_id || '1234' + params.transfer_method = params.transfer_method || 'paypal' + + return models.Transfer.create(params).then(transfer => { + return transfer + }).catch((e) => { + console.log('error on createTransfer', e) + }) +} + +async function truncateModels(model) { + await model.truncate({where: {}, cascade: true, restartIdentity:true}).then(function(rowDeleted){ // rowDeleted will return number of rows deleted + if(rowDeleted === 1){ + console.log('Deleted successfully'); + } + }, function(err){ + console.log(err); + }); +} + module.exports = { register, login, registerAndLogin, + createTask, + createOrder, + createAssign, + createTransfer, + truncateModels } \ No newline at end of file diff --git a/test/task.test.js b/test/task.test.js index 791946169..e1bc9a194 100644 --- a/test/task.test.js +++ b/test/task.test.js @@ -48,7 +48,7 @@ describe("tasks", () => { nock.cleanAll() }) - xdescribe('list tasks', () => { + describe('list tasks', () => { it('should list tasks', (done) => { agent .get('/tasks/list') diff --git a/test/transfer.test.js b/test/transfer.test.js index acdf4acb7..6839264c8 100644 --- a/test/transfer.test.js +++ b/test/transfer.test.js @@ -6,15 +6,144 @@ const chai = require('chai') const spies = require('chai-spies') const api = require('../server') const agent = request.agent(api) -const { registerAndLogin } = require('./helpers') +const nock = require('nock') +const { registerAndLogin, createTask, createOrder, createAssign, createTransfer, truncateModels } = require('./helpers') +const { create } = require('core-js/core/object') +const models = require('../models') +const transfer = require('./data/transfer').transfer.data.object + +// Common function to create transfer +const createTransferWithTaskData = async (taskData, userId, transferId) => { + const res = await agent + .post('/transfers/create') + .send({ + taskId: taskData.id, + userId: userId, + transfer_id: transferId + }); + return res; +} describe("Transfer", () => { describe("Initial transfer with one credit card and account activated", () => { - it("should create a new single transfer", (done) => { - registerAndLogin(agent).then((res) => { - expect(res.text).to.contain('token') - done(); - }); + beforeEach(async () => { + await truncateModels(models.Task); + await truncateModels(models.User); + await truncateModels(models.Assign); + await truncateModels(models.Order); + await truncateModels(models.Transfer); + }) + afterEach(async () => { + nock.cleanAll() + }) + it("should not create transfer with no orders", async () => { + try { + const task = await createTask(agent); + const taskData = task.dataValues; + const assign = await createAssign(agent, {taskId: taskData.id}); + const res = await createTransferWithTaskData(taskData, taskData.userId); + console.log('assign, assign', assign) + expect(res.body).to.exist; + expect(res.body.error).to.equal('No orders found'); + } catch (e) { + console.log('error on transfer', e); + throw e; + } + }) + it("should not create a transfer with no user assigned", async () => { + try { + const task = await createTask(agent); + const taskData = task.dataValues; + const res = await createTransferWithTaskData(taskData, taskData.userId); + expect(res.body).to.exist; + expect(res.body.error).to.equal('No user assigned'); + } catch (e) { + console.log('error on transfer', e); + throw e; + } + }) + it("should not create transfer with no paid order", async () => { + 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 res = await createTransferWithTaskData(taskData, taskData.userId); + expect(res.body).to.exist; + expect(res.body.error).to.equal('All orders must be paid'); + } catch (e) { + console.log('error on transfer', e); + throw e; + } + }) + it("should create transfer with a single order paid with stripe", async () => { + try { + await nock('https://api.stripe.com') + .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 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('200'); + expect(res.body.transfer_method).to.equal('stripe'); + expect(res.body.transfer_id).to.exist; + expect(res.body.transfer_id).to.equal('tr_1CcGcaBrSjgsps2DGToaoNF5'); + } catch (e) { + console.log('error on transfer', e); + throw e; + } + }) + it("should search transfers", async () => { + 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 res = await agent + .get('/transfers/search') + .query({userId: taskData.userId}); + expect(res.body).to.exist; + expect(res.body.length).to.equal(1); + } catch (e) { + console.log('error on transfer', e); + throw e; + } + }) + 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 res1 = await createTransferWithTaskData(taskData, taskData.userId, '123'); + const res2 = await createTransferWithTaskData(taskData, undefined, '123'); + expect(res2.body).to.exist; + expect(res2.body.error).to.equal('This transfer already exists'); + } catch (e) { + console.log('error on transfer', e); + throw e; + } + }) + it("should not create transfers with same taskId", 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 res1 = await createTransferWithTaskData(taskData, taskData.userId); + const res2 = await createTransferWithTaskData(taskData, taskData.userId); + expect(res2.body).to.exist; + expect(res2.body.error).to.equal('Only one transfer for an issue'); + } catch (e) { + console.log('error on transfer', e); + throw e; + } }) }) -}) \ No newline at end of file +}) diff --git a/test/webhook.test.js b/test/webhook.test.js index 1a8558050..07fbf35d6 100644 --- a/test/webhook.test.js +++ b/test/webhook.test.js @@ -4,11 +4,14 @@ const request = require('supertest') const expect = require('chai').expect const api = require('../server') const agent = request.agent(api) +const nock = require('nock') +const { truncateModels, createTask, createAssign, createTransfer, createOrder } = require('./helpers') const models = require('../models') const chargeData = require('./data/charge') const transferData = require('./data/transfer') const payoutData = require('./data/payout') +const balanceTransactionData = require('./data/balance.transaction') const cardData = require('./data/card') const balanceData = require('./data/balance') const refundData = require('./data/refund') @@ -19,30 +22,13 @@ const invoiceUpdated = require('./data/stripe.invoice.update') const invoiceCreated = require('./data/stripe.invoice.create') const invoicePaid = require('./data/stripe.invoice.paid') -xdescribe('webhooks', () => { - beforeEach(() => { - models.Task.destroy({ where: {}, truncate: true, cascade: true }).then( - function (rowDeleted) { - // rowDeleted will return number of rows deleted - if (rowDeleted === 1) { - console.log('Deleted successfully') - } - }, - function (err) { - console.log(err) - } - ) - models.User.destroy({ where: {}, truncate: true, cascade: true }).then( - function (rowDeleted) { - // rowDeleted will return number of rows deleted - if (rowDeleted === 1) { - console.log('Deleted successfully') - } - }, - function (err) { - console.log(err) - } - ) +describe('webhooks', () => { + beforeEach(async () => { + await truncateModels(models.Task); + await truncateModels(models.User); + await truncateModels(models.Assign); + await truncateModels(models.Order); + await truncateModels(models.Transfer); }) describe('webhooks for charge', () => { @@ -315,11 +301,11 @@ xdescribe('webhooks', () => { .createAssign({ userId: user.dataValues.id }) .then(assign => { task - .updateAttributes({ assigned: assign.dataValues.id }) + .update({ assigned: assign.dataValues.id }, { where: { id: task.id } }) .then(updatedTask => { agent .post('/webhooks') - .send(transferData.update) + .send(transferData.transfer) .expect('Content-Type', /json/) .expect(200) .end((err, res) => { @@ -380,26 +366,34 @@ xdescribe('webhooks', () => { }).catch(done) }) - it('should notify the transfer when a webhook payout.done is triggered', done => { - models.User.build({ + it('should notify the transfer and update transfer when a webhook payout.done is triggered', async () => { + await nock('https://api.stripe.com') + .persist() + .get('/v1/balance_transactions/txn_1CdprOLlCJ9CeQRe7gBPy9Lo') + .reply(200, balanceTransactionData.get ); + + const user = await models.User.build({ email: 'teste@mail.com', password: 'teste', account_id: 'acct_1CZ5vkLlCJ9CeQRe' - }) - .save() - .then(user => { - agent - .post('/webhooks') - .send(payoutData.done) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - expect(res.statusCode).to.equal(200) - expect(res.body).to.exist - expect(res.body.id).to.equal('evt_1CeM4PLlCJ9CeQReQrtxB9GJ') - done(err) - }) - }).catch(done) + }).save() + + const task = await createTask(agent) + const taskData = task.dataValues + const createOrder = await task.createOrder({ userId: taskData.userId, TaskId: taskData.id, paid: true, provider: 'stripe' }) + const assign = await createAssign(agent, {taskId: taskData.id}) + const newTransfer = await createTransfer({userId: taskData.userId, taskId: taskData.id, transfer_id: 'tr_1CZ5vkLlCJ9CeQRe', to: assign.dataValues.userId, status: 'pending'}) + const res = await agent + .post('/webhooks') + .send(payoutData.done) + .expect('Content-Type', /json/) + .expect(200) + const currentTransfer = await models.Transfer.findOne({where: {id: newTransfer.dataValues.id}}) + expect(currentTransfer.status).to.equal('paid') + expect(res.statusCode).to.equal(200) + expect(res.body).to.exist + expect(res.body.id).to.equal('evt_1CeM4PLlCJ9CeQReQrtxB9GJ') + }) }) })