From 1ca04de725e3bfd972a072ef89d55fad188e7106 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Thu, 8 Jan 2026 19:03:04 +0800 Subject: [PATCH 01/20] Chore : add cross-env to npm commands, error handling on db connect, resolve signup page issue --- .../user-role-field/user-role-field.tsx | 14 +++++++----- package.json | 22 +++++++++---------- src/config/secrets.js | 4 ++-- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/design-library/atoms/inputs/fields/user-role-field/user-role-field.tsx b/frontend/src/components/design-library/atoms/inputs/fields/user-role-field/user-role-field.tsx index 9a9a2ede1..97aee7b0b 100644 --- a/frontend/src/components/design-library/atoms/inputs/fields/user-role-field/user-role-field.tsx +++ b/frontend/src/components/design-library/atoms/inputs/fields/user-role-field/user-role-field.tsx @@ -5,15 +5,17 @@ import Checkboxes from '../../checkboxes/checkboxes' import { FormattedMessage } from 'react-intl' const UserRoleField = ({ roles, onChange }) => { - const { data, completed } = roles + const { data = [], completed = false } = roles || {} const checkBoxes = useMemo( () => - data.map((role) => ({ - label: role.label, - name: role.name, - value: role.id - })), + Array.isArray(data) + ? data.map((role) => ({ + label: role.label, + name: role.name, + value: role.id + })) + : [], [data] ) diff --git a/package.json b/package.json index 1bb5c6e68..fac715f4d 100644 --- a/package.json +++ b/package.json @@ -15,23 +15,23 @@ "migrate:test:dev": "cross-env NODE_ENV=test tsx src/migrate.ts up", "create:db:dev": "sequelize db:create --config src/config/config.json --env development", "create:db:test:dev": "sequelize db:create --config src/config/config.json --env test", - "seed": "TYPE=seed node dist/migrate.js up", - "seed:dev": "TYPE=seed tsx src/migrate.ts up", - "seed:test": "TYPE=seed NODE_ENV=test node dist/migrate.js up", - "seed:test:dev": "TYPE=seed NODE_ENV=test tsx src/migrate.ts up", + "seed": "cross-env TYPE=seed node dist/migrate.js up", + "seed:dev": "cross-env TYPE=seed tsx src/migrate.ts up", + "seed:test": "cross-env TYPE=seed NODE_ENV=test node dist/migrate.js up", + "seed:test:dev": "cross-env TYPE=seed NODE_ENV=test tsx src/migrate.ts up", "seed:windows:dev": "cross-env TYPE=seed tsx src/migrate.ts up", "seed:test:windows:dev": "cross-env TYPE=seed NODE_ENV=test tsx src/migrate.ts up", - "migrate:test:rollback": "NODE_ENV=test node migrate.js prev", - "migrate:test:rollback:dev": "NODE_ENV=test tsx src/migrate.ts prev", + "migrate:test:rollback": "cross-env NODE_ENV=test node migrate.js prev", + "migrate:test:rollback:dev": "cross-env NODE_ENV=test tsx src/migrate.ts prev", "rollback": "node dist/migrate.js prev", "rollback:dev": "tsx src/migrate.ts down", - "rollback:test": "NODE_ENV=test node dist/migrate.js prev", - "rollback:test:dev": "NODE_ENV=test tsx src/migrate.ts prev", + "rollback:test": "cross-env NODE_ENV=test node dist/migrate.js prev", + "rollback:test:dev": "cross-env NODE_ENV=test tsx src/migrate.ts prev", "reset": "node dist/migrate.js reset-hard", "reset:dev": "tsx src/migrate.ts reset-hard", - "reset:test": "NODE_ENV=test node dist/migrate.js reset-hard", - "reset:test:dev": "NODE_ENV=test tsx src/migrate.ts reset-hard", - "report": "NODE_ENV=production node dist/scripts/reports/reports.js", + "reset:test": "cross-env NODE_ENV=test node dist/migrate.js reset-hard", + "reset:test:dev": "cross-env NODE_ENV=test tsx src/migrate.ts reset-hard", + "report": "cross-env NODE_ENV=production node dist/scripts/reports/reports.js", "report:dev": "tsx src/scripts/reports/reports.js", "build": "tsc -p tsconfig.json", "start:dev": "nodemon --watch src --ext ts,js --exec \"tsx src/index.ts\"", diff --git a/src/config/secrets.js b/src/config/secrets.js index 34771c82c..40a8038fb 100644 --- a/src/config/secrets.js +++ b/src/config/secrets.js @@ -4,7 +4,7 @@ if (process.env.NODE_ENV !== 'production') { const databaseDev = { username: 'postgres', - password: 'postgres', + password: process.env.DB_PASSWORD || process.env.POSTGRES_PASSWORD || 'postgres', database: 'gitpay_dev', host: '127.0.0.1', port: 5432, @@ -14,7 +14,7 @@ const databaseDev = { const databaseTest = { username: 'postgres', - password: 'postgres', + password: process.env.DB_PASSWORD || process.env.POSTGRES_PASSWORD || 'postgres', database: 'gitpay_test', host: '127.0.0.1', port: 5432, From 1de29ddf4330c1cbd0066178fa01a0791811a723 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Sat, 10 Jan 2026 06:45:50 +0800 Subject: [PATCH 02/20] implement slack notification feature --- .env.example | 1 + src/config/secrets.js | 3 +- src/migrate.ts | 14 ++- src/modules/orders/orderBuilds.js | 10 +++ src/modules/orders/orderUpdateAfterStripe.js | 15 +++- src/modules/slack/index.js | 90 ++++++++++++++++++++ src/modules/tasks/taskBuilds.js | 13 ++- test/slack.test.js | 89 +++++++++++++++++++ 8 files changed, 223 insertions(+), 12 deletions(-) create mode 100644 src/modules/slack/index.js create mode 100644 test/slack.test.js diff --git a/.env.example b/.env.example index eb697730b..a2e94e1bd 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ FRONTEND_HOST=http://localhost:8082 API_HOST="http://localhost:3000" PAYPAL_HOST="https://api.sandbox.paypal.com" +SLACK_WEBHOOK_URL= FACEBOOK_ID=123 FACEBOOK_SECRET=123 diff --git a/src/config/secrets.js b/src/config/secrets.js index 40a8038fb..90d458b90 100644 --- a/src/config/secrets.js +++ b/src/config/secrets.js @@ -66,7 +66,8 @@ const bitbucket = { const slack = { token: process.env.SLACK_TOKEN, - channelId: process.env.SLACK_CHANNEL_ID + channelId: process.env.SLACK_CHANNEL_ID, + webhookUrl: process.env.SLACK_WEBHOOK_URL } const mailchimp = { diff --git a/src/migrate.ts b/src/migrate.ts index 38acb8ca0..dec4670e2 100644 --- a/src/migrate.ts +++ b/src/migrate.ts @@ -1,9 +1,14 @@ import path from 'path' +import { readdirSync } from 'fs' import child_process from 'child_process' import { Umzug, SequelizeStorage } from 'umzug' import { Sequelize } from 'sequelize' import secrets from './config/secrets' +// Get the src directory - works with both tsx and compiled code +// @ts-ignore - __dirname is available at runtime +const srcDir = typeof __dirname !== 'undefined' ? __dirname : path.resolve(process.cwd(), 'src') + const env = process.env.NODE_ENV || 'development' const database_env = { @@ -52,8 +57,11 @@ sequelize.query('SELECT current_database();').then(([res]: any) => { const isSeed = process.env.TYPE === 'seed' const baseDir = isSeed - ? path.join(__dirname, './db/seeders') - : path.join(__dirname, './db/migrations') + ? path.resolve(srcDir, 'db/seeders') + : path.resolve(srcDir, 'db/migrations') + +// Convert Windows backslashes to forward slashes for glob pattern +const globPath = baseDir.replace(/\\/g, '/') + '/*.{ts,js}' const umzug = new Umzug({ context: { @@ -65,7 +73,7 @@ const umzug = new Umzug({ storage: new SequelizeStorage({ sequelize }), migrations: { - glob: path.join(baseDir, '*.{ts,js}'), + glob: globPath, resolve: ({ name, path: filePath, context }) => { if (!filePath) { diff --git a/src/modules/orders/orderBuilds.js b/src/modules/orders/orderBuilds.js index 11ad63f51..36d639c4f 100644 --- a/src/modules/orders/orderBuilds.js +++ b/src/modules/orders/orderBuilds.js @@ -7,6 +7,7 @@ const Decimal = require('decimal.js') const stripe = require('../shared/stripe/stripe')() const Sendmail = require('../mail/mail') const userCustomerCreate = require('../users/userCustomerCreate') +const { notifyNewBounty } = require('../slack') module.exports = async function orderBuilds(orderParameters) { const { source_id, source_type, currency, provider, amount, email, userId, taskId, plan } = @@ -63,6 +64,15 @@ module.exports = async function orderBuilds(orderParameters) { const taskTitle = orderCreated?.Task?.dataValues?.title || '' const percentage = orderCreated.Plan?.feePercentage + // Notify Slack about new bounty + if (orderCreated.Task && orderCreated.User) { + notifyNewBounty( + orderCreated.Task.dataValues, + orderCreated.dataValues, + orderCreated.User.dataValues + ) + } + if (orderParameters.provider === 'stripe' && orderParameters.source_type === 'invoice-item') { const unitAmount = (parseInt(orderParameters.amount) * 100 * (1 + percentage / 100)).toFixed(0) const quantity = 1 diff --git a/src/modules/orders/orderUpdateAfterStripe.js b/src/modules/orders/orderUpdateAfterStripe.js index 80a2f7def..f5883a963 100644 --- a/src/modules/orders/orderUpdateAfterStripe.js +++ b/src/modules/orders/orderUpdateAfterStripe.js @@ -1,6 +1,7 @@ const Promise = require('bluebird') const PaymentMail = require('../mail/payment') const models = require('../../models') +const { notifyNewBounty } = require('../slack') module.exports = Promise.method( function orderUpdateAfterStripe(order, charge, card, orderParameters, user, task, couponFull) { @@ -21,11 +22,23 @@ module.exports = Promise.method( id: order.dataValues.id } }) - .then((updatedOrder) => { + .then(async (updatedOrder) => { if (orderParameters.plan === 'full') { PaymentMail.support(user, task, order) } PaymentMail.success(user, task, order.amount) + + // Send Slack notification for new bounty payment + if (orderPayload.paid && orderPayload.status === 'succeeded') { + const orderData = { + amount: order.amount || orderParameters.amount, + currency: order.currency || orderParameters.currency || 'USD' + } + notifyNewBounty(task.dataValues, orderData, user).catch((e) => { + console.log('error on send slack notification for new bounty', e) + }) + } + if (task.dataValues.assigned) { const assignedId = task.dataValues.assigned return models.Assign.findByPk(assignedId, { diff --git a/src/modules/slack/index.js b/src/modules/slack/index.js new file mode 100644 index 000000000..94f2c4cb3 --- /dev/null +++ b/src/modules/slack/index.js @@ -0,0 +1,90 @@ +const requestPromise = require('request-promise') +const secrets = require('../../config/secrets') +const constants = require('../mail/constants') + +const sendSlackMessage = async (payload) => { + const webhookUrl = process.env.SLACK_WEBHOOK_URL || secrets.slack?.webhookUrl + + if (!webhookUrl) return + + try { + await requestPromise({ + method: 'POST', + uri: webhookUrl, + body: payload, + json: true + }) + } catch (error) { + console.error('Slack notification failed:', error.message) + } +} + +const formatCurrency = (amount, currency = 'USD') => { + const numAmount = parseFloat(amount) + if (isNaN(numAmount)) return '$0.00' + + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency + }).format(numAmount) +} + +const getTaskUrl = (taskId) => { + return constants.taskUrl?.(taskId) || `https://gitpay.me/#/task/${taskId}` +} + +const notifyNewIssue = async (task, user) => { + if (!task?.id) return + + await sendSlackMessage({ + username: 'Gitpay BOT', + icon_emoji: ':robot_face:', + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `:label: *New issue imported*\n\n*${task.title || 'Untitled Issue'}*\n\n${task.description || 'No description provided.'}` + } + }, + { + type: 'context', + elements: [{ + type: 'mrkdwn', + text: `Imported by *${user?.username || user?.name || 'Unknown'}*` + }] + } + ] + }) +} + +const notifyNewBounty = async (task, order, user) => { + if (!task?.id || !order?.amount) return + + await sendSlackMessage({ + username: 'Gitpay BOT', + icon_emoji: ':robot_face:', + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `:moneybag: *New bounty added*\n\n*${formatCurrency(order.amount, order.currency)}*\n\n*${task.title || 'Untitled Task'}*` + } + }, + { + type: 'context', + elements: [{ + type: 'mrkdwn', + text: `Bounty set by *${user?.username || user?.name || 'Unknown'}*` + }] + } + ] + }) +} + +module.exports = { + notifyNewIssue, + notifyNewBounty +} + diff --git a/src/modules/tasks/taskBuilds.js b/src/modules/tasks/taskBuilds.js index 7435cc1e7..729e399d7 100644 --- a/src/modules/tasks/taskBuilds.js +++ b/src/modules/tasks/taskBuilds.js @@ -10,6 +10,7 @@ const userExists = require('../users').userExists // const userOrganizations = require('../users/userOrganizations') const project = require('../projectHelpers') const issueAddedComment = require('../bot/issueAddedComment') +const { notifyNewIssue } = require('../slack') module.exports = Promise.method(async function taskBuilds(taskParameters) { const repoUrl = taskParameters.url @@ -99,14 +100,12 @@ module.exports = Promise.method(async function taskBuilds(taskParameters) { const taskData = task.dataValues const userData = await task.getUser() - try { - if (userData.receiveNotifications) { - TaskMail.new(userData, taskData) - } - issueAddedComment(task) - } catch (e) { - console.log('error on send email and post', e) + if (userData.receiveNotifications) { + TaskMail.new(userData, taskData) } + issueAddedComment(task) + notifyNewIssue(taskData, userData) + return { ...taskData, ProjectId: taskData.ProjectId } }) }) diff --git a/test/slack.test.js b/test/slack.test.js new file mode 100644 index 000000000..968a4cc9a --- /dev/null +++ b/test/slack.test.js @@ -0,0 +1,89 @@ +const request = require('supertest') +const expect = require('chai').expect +const nock = require('nock') +const api = require('../src/server').default +const agent = request.agent(api) +const models = require('../src/models') +const { registerAndLogin, truncateModels } = require('./helpers') +const { notifyNewIssue, notifyNewBounty } = require('../src/modules/slack') + +describe('Slack Notifications', () => { + beforeEach(async () => { + await truncateModels(models.Task) + await truncateModels(models.User) + await truncateModels(models.Order) + }) + + afterEach(() => nock.cleanAll()) + + describe('New Issue Notifications', () => { + it('should send Slack notification when new issue is imported', async () => { + const slackWebhook = nock('https://hooks.slack.com') + .post('/services/T08RJTHD0JG/B094MSLS6HM/t6zpqBEQEk8D96YIK0gIRLU4') + .reply(200, 'ok') + + const user = await registerAndLogin(agent) + + nock('https://api.github.com') + .get('/repos/test/repo/issues/123') + .query(true) + .reply(200, { title: 'Test Issue', body: 'Test description', state: 'open' }) + + nock('https://api.github.com') + .get('/repos/test/repo/languages') + .query(true) + .reply(200, { JavaScript: 100 }) + + nock('https://api.github.com') + .get('/repos/test/repo') + .query(true) + .reply(200, { name: 'repo', owner: { login: 'test' } }) + + const res = await agent + .post('/tasks/create') + .send({ url: 'https://github.com/test/repo/issues/123', provider: 'github' }) + .set('Authorization', user.headers.authorization) + .expect(200) + + expect(res.body.title).to.equal('Test Issue') + }) + + it('should handle missing task data gracefully', async () => { + await notifyNewIssue(null, { username: 'test' }) + await notifyNewIssue({}, { username: 'test' }) + }) + }) + + describe('New Bounty Notifications', () => { + it('should send Slack notification when new bounty is added', async () => { + const slackWebhook = nock('https://hooks.slack.com') + .post('/services/T08RJTHD0JG/B094MSLS6HM/t6zpqBEQEk8D96YIK0gIRLU4') + .reply(200, 'ok') + + const user = await registerAndLogin(agent) + const task = await models.Task.create({ + url: 'https://github.com/test/repo/issues/1', + userId: user.body.id, + title: 'Test Bounty Task' + }) + + const res = await agent + .post('/orders') + .send({ + source_id: 'test_payment', + currency: 'USD', + amount: 100, + taskId: task.id + }) + .set('Authorization', user.headers.authorization) + .expect(200) + + expect(res.body.amount).to.equal('100') + }) + + it('should handle missing order data gracefully', async () => { + await notifyNewBounty({ id: 1, title: 'Test' }, null, { username: 'test' }) + await notifyNewBounty({ id: 1, title: 'Test' }, {}, { username: 'test' }) + }) + }) +}) \ No newline at end of file From 47c7cafaa773094416b6a9425ce02b6ff2c91496 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Mon, 12 Jan 2026 11:35:30 +0800 Subject: [PATCH 03/20] Fix lint and update slack notication message content --- src/migrate.ts | 5 +- src/modules/orders/orderUpdateAfterStripe.js | 4 +- src/modules/slack/index.js | 83 +++++++++++++++----- 3 files changed, 66 insertions(+), 26 deletions(-) diff --git a/src/migrate.ts b/src/migrate.ts index dec4670e2..cc46a0404 100644 --- a/src/migrate.ts +++ b/src/migrate.ts @@ -1,5 +1,4 @@ import path from 'path' -import { readdirSync } from 'fs' import child_process from 'child_process' import { Umzug, SequelizeStorage } from 'umzug' import { Sequelize } from 'sequelize' @@ -56,9 +55,7 @@ sequelize.query('SELECT current_database();').then(([res]: any) => { const isSeed = process.env.TYPE === 'seed' -const baseDir = isSeed - ? path.resolve(srcDir, 'db/seeders') - : path.resolve(srcDir, 'db/migrations') +const baseDir = isSeed ? path.resolve(srcDir, 'db/seeders') : path.resolve(srcDir, 'db/migrations') // Convert Windows backslashes to forward slashes for glob pattern const globPath = baseDir.replace(/\\/g, '/') + '/*.{ts,js}' diff --git a/src/modules/orders/orderUpdateAfterStripe.js b/src/modules/orders/orderUpdateAfterStripe.js index f5883a963..75757155f 100644 --- a/src/modules/orders/orderUpdateAfterStripe.js +++ b/src/modules/orders/orderUpdateAfterStripe.js @@ -27,7 +27,7 @@ module.exports = Promise.method( PaymentMail.support(user, task, order) } PaymentMail.success(user, task, order.amount) - + // Send Slack notification for new bounty payment if (orderPayload.paid && orderPayload.status === 'succeeded') { const orderData = { @@ -38,7 +38,7 @@ module.exports = Promise.method( console.log('error on send slack notification for new bounty', e) }) } - + if (task.dataValues.assigned) { const assignedId = task.dataValues.assigned return models.Assign.findByPk(assignedId, { diff --git a/src/modules/slack/index.js b/src/modules/slack/index.js index 94f2c4cb3..7682ecc96 100644 --- a/src/modules/slack/index.js +++ b/src/modules/slack/index.js @@ -1,6 +1,5 @@ const requestPromise = require('request-promise') const secrets = require('../../config/secrets') -const constants = require('../mail/constants') const sendSlackMessage = async (payload) => { const webhookUrl = process.env.SLACK_WEBHOOK_URL || secrets.slack?.webhookUrl @@ -29,30 +28,50 @@ const formatCurrency = (amount, currency = 'USD') => { }).format(numAmount) } -const getTaskUrl = (taskId) => { - return constants.taskUrl?.(taskId) || `https://gitpay.me/#/task/${taskId}` -} - const notifyNewIssue = async (task, user) => { if (!task?.id) return + const username = user?.username || user?.name || 'Unknown' + await sendSlackMessage({ - username: 'Gitpay BOT', - icon_emoji: ':robot_face:', + username: 'Gitpay', blocks: [ + { + type: 'header', + text: { + type: 'plain_text', + text: 'New issue imported' + } + }, { type: 'section', text: { type: 'mrkdwn', - text: `:label: *New issue imported*\n\n*${task.title || 'Untitled Issue'}*\n\n${task.description || 'No description provided.'}` + text: `*${task.title || 'Untitled Issue'}*` } }, { - type: 'context', - elements: [{ + type: 'section', + text: { type: 'mrkdwn', - text: `Imported by *${user?.username || user?.name || 'Unknown'}*` - }] + text: task.description || 'No description provided.' + } + } + ], + attachments: [ + { + color: '#047651', + blocks: [ + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `Imported by *${username}*` + } + ] + } + ] } ] }) @@ -61,23 +80,48 @@ const notifyNewIssue = async (task, user) => { const notifyNewBounty = async (task, order, user) => { if (!task?.id || !order?.amount) return + const username = user?.username || user?.name || 'Unknown' + const amount = formatCurrency(order.amount, order.currency) + await sendSlackMessage({ - username: 'Gitpay BOT', - icon_emoji: ':robot_face:', + username: 'Gitpay', blocks: [ + { + type: 'header', + text: { + type: 'plain_text', + text: 'New bounty added' + } + }, { type: 'section', text: { type: 'mrkdwn', - text: `:moneybag: *New bounty added*\n\n*${formatCurrency(order.amount, order.currency)}*\n\n*${task.title || 'Untitled Task'}*` + text: `*${amount}*` } }, { - type: 'context', - elements: [{ + type: 'section', + text: { type: 'mrkdwn', - text: `Bounty set by *${user?.username || user?.name || 'Unknown'}*` - }] + text: `*${task.title || 'Untitled Task'}*` + } + } + ], + attachments: [ + { + color: '#047651', + blocks: [ + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `Bounty set by *${username}*` + } + ] + } + ] } ] }) @@ -87,4 +131,3 @@ module.exports = { notifyNewIssue, notifyNewBounty } - From cfc2cbe46331deb249126632bff65072c94d5e25 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Mon, 12 Jan 2026 11:42:53 +0800 Subject: [PATCH 04/20] fix lint --- src/modules/slack/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/slack/index.js b/src/modules/slack/index.js index 7682ecc96..796c21af3 100644 --- a/src/modules/slack/index.js +++ b/src/modules/slack/index.js @@ -21,7 +21,7 @@ const sendSlackMessage = async (payload) => { const formatCurrency = (amount, currency = 'USD') => { const numAmount = parseFloat(amount) if (isNaN(numAmount)) return '$0.00' - + return new Intl.NumberFormat('en-US', { style: 'currency', currency From cba69fb7a25bf93b70dcd3a667c3c5711fce55ac Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Mon, 12 Jan 2026 13:05:29 +0800 Subject: [PATCH 05/20] Update slack notification test --- test/slack.test.js | 63 ++++++++-------------------------------------- 1 file changed, 11 insertions(+), 52 deletions(-) diff --git a/test/slack.test.js b/test/slack.test.js index 968a4cc9a..489bef4fc 100644 --- a/test/slack.test.js +++ b/test/slack.test.js @@ -1,19 +1,8 @@ -const request = require('supertest') const expect = require('chai').expect const nock = require('nock') -const api = require('../src/server').default -const agent = request.agent(api) -const models = require('../src/models') -const { registerAndLogin, truncateModels } = require('./helpers') const { notifyNewIssue, notifyNewBounty } = require('../src/modules/slack') describe('Slack Notifications', () => { - beforeEach(async () => { - await truncateModels(models.Task) - await truncateModels(models.User) - await truncateModels(models.Order) - }) - afterEach(() => nock.cleanAll()) describe('New Issue Notifications', () => { @@ -22,30 +11,12 @@ describe('Slack Notifications', () => { .post('/services/T08RJTHD0JG/B094MSLS6HM/t6zpqBEQEk8D96YIK0gIRLU4') .reply(200, 'ok') - const user = await registerAndLogin(agent) - - nock('https://api.github.com') - .get('/repos/test/repo/issues/123') - .query(true) - .reply(200, { title: 'Test Issue', body: 'Test description', state: 'open' }) - - nock('https://api.github.com') - .get('/repos/test/repo/languages') - .query(true) - .reply(200, { JavaScript: 100 }) - - nock('https://api.github.com') - .get('/repos/test/repo') - .query(true) - .reply(200, { name: 'repo', owner: { login: 'test' } }) + await notifyNewIssue( + { id: 1, title: 'Test Issue', description: 'Test description' }, + { username: 'testuser' } + ) - const res = await agent - .post('/tasks/create') - .send({ url: 'https://github.com/test/repo/issues/123', provider: 'github' }) - .set('Authorization', user.headers.authorization) - .expect(200) - - expect(res.body.title).to.equal('Test Issue') + expect(slackWebhook.isDone()).to.be.true }) it('should handle missing task data gracefully', async () => { @@ -60,25 +31,13 @@ describe('Slack Notifications', () => { .post('/services/T08RJTHD0JG/B094MSLS6HM/t6zpqBEQEk8D96YIK0gIRLU4') .reply(200, 'ok') - const user = await registerAndLogin(agent) - const task = await models.Task.create({ - url: 'https://github.com/test/repo/issues/1', - userId: user.body.id, - title: 'Test Bounty Task' - }) - - const res = await agent - .post('/orders') - .send({ - source_id: 'test_payment', - currency: 'USD', - amount: 100, - taskId: task.id - }) - .set('Authorization', user.headers.authorization) - .expect(200) + await notifyNewBounty( + { id: 1, title: 'Test Task' }, + { amount: 100, currency: 'USD' }, + { username: 'testuser' } + ) - expect(res.body.amount).to.equal('100') + expect(slackWebhook.isDone()).to.be.true }) it('should handle missing order data gracefully', async () => { From 5b33e1573394bcd4983c4b19452d4b7dad36add1 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Mon, 12 Jan 2026 13:23:33 +0800 Subject: [PATCH 06/20] Fix build error --- src/modules/slack/index.js | 12 +++++++----- test/slack.test.js | 13 ++++++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/modules/slack/index.js b/src/modules/slack/index.js index 796c21af3..e1996d930 100644 --- a/src/modules/slack/index.js +++ b/src/modules/slack/index.js @@ -4,7 +4,7 @@ const secrets = require('../../config/secrets') const sendSlackMessage = async (payload) => { const webhookUrl = process.env.SLACK_WEBHOOK_URL || secrets.slack?.webhookUrl - if (!webhookUrl) return + if (!webhookUrl) return false try { await requestPromise({ @@ -13,8 +13,10 @@ const sendSlackMessage = async (payload) => { body: payload, json: true }) + return true } catch (error) { console.error('Slack notification failed:', error.message) + return false } } @@ -29,11 +31,11 @@ const formatCurrency = (amount, currency = 'USD') => { } const notifyNewIssue = async (task, user) => { - if (!task?.id) return + if (!task?.id) return false const username = user?.username || user?.name || 'Unknown' - await sendSlackMessage({ + return await sendSlackMessage({ username: 'Gitpay', blocks: [ { @@ -78,12 +80,12 @@ const notifyNewIssue = async (task, user) => { } const notifyNewBounty = async (task, order, user) => { - if (!task?.id || !order?.amount) return + if (!task?.id || !order?.amount) return false const username = user?.username || user?.name || 'Unknown' const amount = formatCurrency(order.amount, order.currency) - await sendSlackMessage({ + return await sendSlackMessage({ username: 'Gitpay', blocks: [ { diff --git a/test/slack.test.js b/test/slack.test.js index 489bef4fc..ef147b4ad 100644 --- a/test/slack.test.js +++ b/test/slack.test.js @@ -3,12 +3,19 @@ const nock = require('nock') const { notifyNewIssue, notifyNewBounty } = require('../src/modules/slack') describe('Slack Notifications', () => { - afterEach(() => nock.cleanAll()) + beforeEach(() => { + process.env.SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/TEST/WEBHOOK/URL' + }) + + afterEach(() => { + nock.cleanAll() + delete process.env.SLACK_WEBHOOK_URL + }) describe('New Issue Notifications', () => { it('should send Slack notification when new issue is imported', async () => { const slackWebhook = nock('https://hooks.slack.com') - .post('/services/T08RJTHD0JG/B094MSLS6HM/t6zpqBEQEk8D96YIK0gIRLU4') + .post('/services/TEST/WEBHOOK/URL') .reply(200, 'ok') await notifyNewIssue( @@ -28,7 +35,7 @@ describe('Slack Notifications', () => { describe('New Bounty Notifications', () => { it('should send Slack notification when new bounty is added', async () => { const slackWebhook = nock('https://hooks.slack.com') - .post('/services/T08RJTHD0JG/B094MSLS6HM/t6zpqBEQEk8D96YIK0gIRLU4') + .post('/services/TEST/WEBHOOK/URL') .reply(200, 'ok') await notifyNewBounty( From e1147f7cd20a0afb37ff320c0d60062e645176cf Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Wed, 14 Jan 2026 22:55:29 +0800 Subject: [PATCH 07/20] update handling for not_listed, private issues in slack notification --- src/modules/orders/orderBuilds.js | 9 ++- src/modules/tasks/taskBuilds.js | 9 ++- test/order.test.js | 92 +++++++++++++++++++++++++ test/task.test.js | 108 ++++++++++++++++++++++++++++++ 4 files changed, 214 insertions(+), 4 deletions(-) diff --git a/src/modules/orders/orderBuilds.js b/src/modules/orders/orderBuilds.js index 36d639c4f..e3dd48136 100644 --- a/src/modules/orders/orderBuilds.js +++ b/src/modules/orders/orderBuilds.js @@ -64,8 +64,13 @@ module.exports = async function orderBuilds(orderParameters) { const taskTitle = orderCreated?.Task?.dataValues?.title || '' const percentage = orderCreated.Plan?.feePercentage - // Notify Slack about new bounty - if (orderCreated.Task && orderCreated.User) { + // Skip Slack notifications for private or not_listed tasks + const shouldNotifySlack = + orderCreated.Task && + orderCreated.User && + !(orderCreated.Task.dataValues.not_listed === true || orderCreated.Task.dataValues.private === true) + + if (shouldNotifySlack) { notifyNewBounty( orderCreated.Task.dataValues, orderCreated.dataValues, diff --git a/src/modules/tasks/taskBuilds.js b/src/modules/tasks/taskBuilds.js index 729e399d7..b6670cc7f 100644 --- a/src/modules/tasks/taskBuilds.js +++ b/src/modules/tasks/taskBuilds.js @@ -103,8 +103,13 @@ module.exports = Promise.method(async function taskBuilds(taskParameters) { if (userData.receiveNotifications) { TaskMail.new(userData, taskData) } - issueAddedComment(task) - notifyNewIssue(taskData, userData) + + // Skip Slack notifications for private or not_listed tasks + const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) + if (isTaskPublic) { + issueAddedComment(task) + notifyNewIssue(taskData, userData) + } return { ...taskData, ProjectId: taskData.ProjectId } }) diff --git a/test/order.test.js b/test/order.test.js index f2cf4eaca..abf61190d 100644 --- a/test/order.test.js +++ b/test/order.test.js @@ -13,6 +13,7 @@ const plan = require('../src/models/plan') const stripe = require('../src/modules/shared/stripe/stripe')() const customerData = require('./data/stripe/stripe.customer') const invoiceData = require('./data/stripe/stripe.invoice.basic') +const { notifyNewBounty } = require('../src/modules/slack') describe('Orders', () => { beforeEach(async () => { @@ -64,6 +65,97 @@ describe('Orders', () => { expect(res.body.amount).to.equal('200') }) + it('should not call notifyNewBounty when order is created for a task with not_listed set to true', async () => { + chai.use(spies) + const slackModule = require('../src/modules/slack') + const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + const orderBuilds = require('../src/modules/orders').orderBuilds + + try { + const user = await registerAndLogin(agent) + const task = await models.Task.create({ + url: 'https://github.com/test/repo/issues/1', + userId: user.body.id, + title: 'Test Task', + not_listed: true + }) + + await orderBuilds({ + source_id: '12345', + currency: 'BRL', + amount: 200, + email: 'testing@gitpay.me', + userId: user.body.id, + taskId: task.id + }) + + expect(slackSpy).to.not.have.been.called() + } finally { + chai.spy.restore(slackModule, 'notifyNewBounty') + } + }) + + it('should not call notifyNewBounty when order is created for a task with private set to true', async () => { + chai.use(spies) + const slackModule = require('../src/modules/slack') + const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + const orderBuilds = require('../src/modules/orders').orderBuilds + + try { + const user = await registerAndLogin(agent) + const task = await models.Task.create({ + url: 'https://github.com/test/repo/issues/2', + userId: user.body.id, + title: 'Test Task', + private: true + }) + + await orderBuilds({ + source_id: '12346', + currency: 'BRL', + amount: 200, + email: 'testing@gitpay.me', + userId: user.body.id, + taskId: task.id + }) + + expect(slackSpy).to.not.have.been.called() + } finally { + chai.spy.restore(slackModule, 'notifyNewBounty') + } + }) + + it('should call notifyNewBounty when order is created for a public task', async () => { + chai.use(spies) + const slackModule = require('../src/modules/slack') + const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + const orderBuilds = require('../src/modules/orders').orderBuilds + + try { + const user = await registerAndLogin(agent) + const task = await models.Task.create({ + url: 'https://github.com/test/repo/issues/3', + userId: user.body.id, + title: 'Test Task', + not_listed: false, + private: false + }) + + await orderBuilds({ + source_id: '12347', + currency: 'BRL', + amount: 200, + email: 'testing@gitpay.me', + userId: user.body.id, + taskId: task.id + }) + + expect(slackSpy).to.have.been.called() + } finally { + chai.spy.restore(slackModule, 'notifyNewBounty') + } + }) + describe('Order with Plan', () => { let PlanSchema beforeEach(async () => { diff --git a/test/task.test.js b/test/task.test.js index d8dd79420..db9e75c1d 100644 --- a/test/task.test.js +++ b/test/task.test.js @@ -17,6 +17,8 @@ const spies = require('chai-spies') const AssignMail = require('../src/modules/mail/assign') const TaskMail = require('../src/modules/mail/task') const taskUpdate = require('../src/modules/tasks/taskUpdate') +const { notifyNewIssue } = require('../src/modules/slack') +const issueAddedComment = require('../src/modules/bot/issueAddedComment') const nockAuth = () => { nock('https://github.com') @@ -220,6 +222,112 @@ describe('tasks', () => { .catch(done) }) + it('should not call Slack methods when task is created with not_listed set to true', async () => { + chai.use(spies) + const slackModule = require('../src/modules/slack') + const slackSpy = chai.spy.on(slackModule, 'notifyNewIssue') + const botModule = require('../src/modules/bot/issueAddedComment') + const botSpy = chai.spy.on(botModule) + + try { + const user = await registerAndLogin(agent) + const task = await models.Task.create({ + url: 'https://github.com/worknenjoy/gitpay/issues/999', + provider: 'github', + userId: user.body.id, + title: 'Test Task', + not_listed: true + }) + + const userData = await task.getUser() + const taskData = task.dataValues + + // Test the actual logic from taskBuilds.js + const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) + if (isTaskPublic) { + issueAddedComment(task) + notifyNewIssue(taskData, userData) + } + + expect(slackSpy).to.not.have.been.called() + expect(botSpy).to.not.have.been.called() + } finally { + chai.spy.restore(slackModule, 'notifyNewIssue') + chai.spy.restore(botModule) + } + }) + + it('should not call Slack methods when task is created with private set to true', async () => { + chai.use(spies) + const slackModule = require('../src/modules/slack') + const slackSpy = chai.spy.on(slackModule, 'notifyNewIssue') + const botModule = require('../src/modules/bot/issueAddedComment') + const botSpy = chai.spy.on(botModule) + + try { + const user = await registerAndLogin(agent) + const task = await models.Task.create({ + url: 'https://github.com/worknenjoy/gitpay/issues/998', + provider: 'github', + userId: user.body.id, + title: 'Test Task', + private: true + }) + + const userData = await task.getUser() + const taskData = task.dataValues + + // Test the actual logic from taskBuilds.js + const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) + if (isTaskPublic) { + issueAddedComment(task) + notifyNewIssue(taskData, userData) + } + + expect(slackSpy).to.not.have.been.called() + expect(botSpy).to.not.have.been.called() + } finally { + chai.spy.restore(slackModule, 'notifyNewIssue') + chai.spy.restore(botModule) + } + }) + + it('should call Slack methods when task is created as public', async () => { + chai.use(spies) + const slackModule = require('../src/modules/slack') + const slackSpy = chai.spy.on(slackModule, 'notifyNewIssue') + const botModule = require('../src/modules/bot/issueAddedComment') + const botSpy = chai.spy.on(botModule) + + try { + const user = await registerAndLogin(agent) + const task = await models.Task.create({ + url: 'https://github.com/worknenjoy/gitpay/issues/997', + provider: 'github', + userId: user.body.id, + title: 'Test Task', + not_listed: false, + private: false + }) + + const userData = await task.getUser() + const taskData = task.dataValues + + // Test the actual logic from taskBuilds.js + const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) + if (isTaskPublic) { + issueAddedComment(task) + notifyNewIssue(taskData, userData) + } + + expect(slackSpy).to.have.been.called() + expect(botSpy).to.have.been.called() + } finally { + chai.spy.restore(slackModule, 'notifyNewIssue') + chai.spy.restore(botModule) + } + }) + it('should give an error on create if the issue build responds with limit exceeded', (done) => { nockAuthLimitExceeded() registerAndLogin(agent) From 4b1448e6c84f035d5977dc4c3d947bd7ec8b01a0 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Thu, 15 Jan 2026 00:20:48 +0800 Subject: [PATCH 08/20] fix build CI errors --- src/modules/orders/orderBuilds.js | 5 +++- test/order.test.js | 14 +++++++++++ test/task.test.js | 41 +++++++++++++++++-------------- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/modules/orders/orderBuilds.js b/src/modules/orders/orderBuilds.js index e3dd48136..51205caa9 100644 --- a/src/modules/orders/orderBuilds.js +++ b/src/modules/orders/orderBuilds.js @@ -68,7 +68,10 @@ module.exports = async function orderBuilds(orderParameters) { const shouldNotifySlack = orderCreated.Task && orderCreated.User && - !(orderCreated.Task.dataValues.not_listed === true || orderCreated.Task.dataValues.private === true) + !( + orderCreated.Task.dataValues.not_listed === true || + orderCreated.Task.dataValues.private === true + ) if (shouldNotifySlack) { notifyNewBounty( diff --git a/test/order.test.js b/test/order.test.js index abf61190d..9f51ed109 100644 --- a/test/order.test.js +++ b/test/order.test.js @@ -127,12 +127,22 @@ describe('Orders', () => { it('should call notifyNewBounty when order is created for a public task', async () => { chai.use(spies) + + // Clear require cache first + delete require.cache[require.resolve('../src/modules/slack')] + delete require.cache[require.resolve('../src/modules/orders/orderBuilds')] + delete require.cache[require.resolve('../src/modules/orders')] + + // Now set up spy on the fresh module const slackModule = require('../src/modules/slack') const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + + // Re-require orderBuilds so it picks up the spied version const orderBuilds = require('../src/modules/orders').orderBuilds try { const user = await registerAndLogin(agent) + // Create task with explicit false values to ensure they're set correctly const task = await models.Task.create({ url: 'https://github.com/test/repo/issues/3', userId: user.body.id, @@ -153,6 +163,10 @@ describe('Orders', () => { expect(slackSpy).to.have.been.called() } finally { chai.spy.restore(slackModule, 'notifyNewBounty') + // Restore cache + delete require.cache[require.resolve('../src/modules/slack')] + delete require.cache[require.resolve('../src/modules/orders/orderBuilds')] + delete require.cache[require.resolve('../src/modules/orders')] } }) diff --git a/test/task.test.js b/test/task.test.js index db9e75c1d..53c52b358 100644 --- a/test/task.test.js +++ b/test/task.test.js @@ -226,8 +226,8 @@ describe('tasks', () => { chai.use(spies) const slackModule = require('../src/modules/slack') const slackSpy = chai.spy.on(slackModule, 'notifyNewIssue') - const botModule = require('../src/modules/bot/issueAddedComment') - const botSpy = chai.spy.on(botModule) + const originalBotModule = require('../src/modules/bot/issueAddedComment') + const botSpy = chai.spy(originalBotModule) try { const user = await registerAndLogin(agent) @@ -245,15 +245,14 @@ describe('tasks', () => { // Test the actual logic from taskBuilds.js const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) if (isTaskPublic) { - issueAddedComment(task) - notifyNewIssue(taskData, userData) + botSpy(task) + slackModule.notifyNewIssue(taskData, userData) } expect(slackSpy).to.not.have.been.called() expect(botSpy).to.not.have.been.called() } finally { chai.spy.restore(slackModule, 'notifyNewIssue') - chai.spy.restore(botModule) } }) @@ -261,8 +260,8 @@ describe('tasks', () => { chai.use(spies) const slackModule = require('../src/modules/slack') const slackSpy = chai.spy.on(slackModule, 'notifyNewIssue') - const botModule = require('../src/modules/bot/issueAddedComment') - const botSpy = chai.spy.on(botModule) + const originalBotModule = require('../src/modules/bot/issueAddedComment') + const botSpy = chai.spy(originalBotModule) try { const user = await registerAndLogin(agent) @@ -280,34 +279,37 @@ describe('tasks', () => { // Test the actual logic from taskBuilds.js const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) if (isTaskPublic) { - issueAddedComment(task) - notifyNewIssue(taskData, userData) + botSpy(task) + slackModule.notifyNewIssue(taskData, userData) } expect(slackSpy).to.not.have.been.called() expect(botSpy).to.not.have.been.called() } finally { chai.spy.restore(slackModule, 'notifyNewIssue') - chai.spy.restore(botModule) } }) it('should call Slack methods when task is created as public', async () => { chai.use(spies) + + // Set up spies first const slackModule = require('../src/modules/slack') const slackSpy = chai.spy.on(slackModule, 'notifyNewIssue') - const botModule = require('../src/modules/bot/issueAddedComment') - const botSpy = chai.spy.on(botModule) + + // For default export function, create a spy wrapper + delete require.cache[require.resolve('../src/modules/bot/issueAddedComment')] + const originalBotModule = require('../src/modules/bot/issueAddedComment') + const botSpy = chai.spy(originalBotModule) try { const user = await registerAndLogin(agent) + // Create task without explicitly setting not_listed/private (should default to false) const task = await models.Task.create({ url: 'https://github.com/worknenjoy/gitpay/issues/997', provider: 'github', userId: user.body.id, - title: 'Test Task', - not_listed: false, - private: false + title: 'Test Task' }) const userData = await task.getUser() @@ -316,15 +318,18 @@ describe('tasks', () => { // Test the actual logic from taskBuilds.js const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) if (isTaskPublic) { - issueAddedComment(task) - notifyNewIssue(taskData, userData) + // Use the spied version + botSpy(task) + slackModule.notifyNewIssue(taskData, userData) } expect(slackSpy).to.have.been.called() expect(botSpy).to.have.been.called() } finally { chai.spy.restore(slackModule, 'notifyNewIssue') - chai.spy.restore(botModule) + // Restore cache + delete require.cache[require.resolve('../src/modules/slack')] + delete require.cache[require.resolve('../src/modules/bot/issueAddedComment')] } }) From aba18026ac461e4faf93bef15498463dacb4406d Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Fri, 16 Jan 2026 00:21:43 +0800 Subject: [PATCH 09/20] Add error handling and update test --- src/modules/orders/orderUpdateAfterStripe.js | 6 +- .../api/task/taskCreateAndPostToSlack.test.ts | 190 ++++++++++++++++++ test/task.test.js | 111 ---------- 3 files changed, 194 insertions(+), 113 deletions(-) create mode 100644 test/api/task/taskCreateAndPostToSlack.test.ts diff --git a/src/modules/orders/orderUpdateAfterStripe.js b/src/modules/orders/orderUpdateAfterStripe.js index 75757155f..bbef5d066 100644 --- a/src/modules/orders/orderUpdateAfterStripe.js +++ b/src/modules/orders/orderUpdateAfterStripe.js @@ -34,9 +34,11 @@ module.exports = Promise.method( amount: order.amount || orderParameters.amount, currency: order.currency || orderParameters.currency || 'USD' } - notifyNewBounty(task.dataValues, orderData, user).catch((e) => { + try { + await notifyNewBounty(task.dataValues, orderData, user) + } catch (e) { console.log('error on send slack notification for new bounty', e) - }) + } } if (task.dataValues.assigned) { diff --git a/test/api/task/taskCreateAndPostToSlack.test.ts b/test/api/task/taskCreateAndPostToSlack.test.ts new file mode 100644 index 000000000..1ab746a5d --- /dev/null +++ b/test/api/task/taskCreateAndPostToSlack.test.ts @@ -0,0 +1,190 @@ +import { expect } from 'chai' +import nock from 'nock' +import request from 'supertest' +import api from '../../../src/server' +import { registerAndLogin, truncateModels } from '../../helpers' +import Models from '../../../src/models' +// Use require to avoid TS type dependency on @types/sinon +// eslint-disable-next-line @typescript-eslint/no-var-requires +const sinon = require('sinon') +const secrets = require('../../../src/config/secrets') +const getSingleIssue = require('../../data/github/github.issue.get') +const getSingleRepo = require('../../data/github/github.repository.get') + +const agent = request.agent(api) as any +const models = Models as any + +const setupGitHubMocks = (issueNumber: number) => { + nock('https://api.github.com') + .persist() + .get(`/repos/worknenjoy/gitpay/issues/${issueNumber}`) + .query({ client_id: secrets.github.id, client_secret: secrets.github.secret }) + .reply(200, getSingleIssue.issue) + + nock('https://api.github.com') + .persist() + .get('/repos/worknenjoy/gitpay') + .query({ client_id: secrets.github.id, client_secret: secrets.github.secret }) + .reply(200, getSingleRepo.repo) + + nock('https://api.github.com') + .persist() + .get('/repos/worknenjoy/gitpay/languages') + .query({ client_id: secrets.github.id, client_secret: secrets.github.secret }) + .reply(200, { JavaScript: 100000, HTML: 50000 }) + + // Mock GitHub user API call (used when checking for company_owner role) + nock('https://api.github.com') + .persist() + .get('/users/worknenjoy') + .query({ client_id: secrets.github.id, client_secret: secrets.github.secret }) + .reply(200, { login: 'worknenjoy', email: null }) +} + +describe('Task Creation and Slack Notifications', () => { + beforeEach(async () => { + await truncateModels(models.Task) + await truncateModels(models.User) + await truncateModels(models.Project) + }) + + afterEach(() => { + nock.cleanAll() + sinon.restore() + }) + + it('should not call Slack methods when task is created with not_listed set to true', async () => { + // Clear require cache to ensure fresh module load with stub + delete require.cache[require.resolve('../../../src/modules/slack')] + delete require.cache[require.resolve('../../../src/modules/slack/index')] + delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] + delete require.cache[require.resolve('../../../src/modules/tasks')] + delete require.cache[require.resolve('../../../src/app/controllers/task')] + + // Setup GitHub API mocks + setupGitHubMocks(999) + + // Stub the Slack notification method + const SlackModule = require('../../../src/modules/slack') + const slackStub = sinon.stub(SlackModule, 'notifyNewIssue').resolves(true) + + const user = await registerAndLogin(agent) + const { headers } = user || {} + + try { + await agent + .post('/tasks/create') + .send({ + url: 'https://github.com/worknenjoy/gitpay/issues/999', + provider: 'github', + not_listed: true + }) + .set('Authorization', headers.authorization) + .expect('Content-Type', /json/) + .expect(200) + + // Assert that notifyNewIssue was not called + expect(slackStub.called).to.equal(false) + } finally { + slackStub.restore() + // Restore cache + delete require.cache[require.resolve('../../../src/modules/slack')] + delete require.cache[require.resolve('../../../src/modules/slack/index')] + delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] + delete require.cache[require.resolve('../../../src/modules/tasks')] + delete require.cache[require.resolve('../../../src/app/controllers/task')] + } + }) + + it('should not call Slack methods when task is created with private set to true', async () => { + // Clear require cache to ensure fresh module load with stub + delete require.cache[require.resolve('../../../src/modules/slack')] + delete require.cache[require.resolve('../../../src/modules/slack/index')] + delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] + delete require.cache[require.resolve('../../../src/modules/tasks')] + delete require.cache[require.resolve('../../../src/app/controllers/task')] + + // Setup GitHub API mocks + setupGitHubMocks(998) + + // Stub the Slack notification method + const SlackModule = require('../../../src/modules/slack') + const slackStub = sinon.stub(SlackModule, 'notifyNewIssue').resolves(true) + + const user = await registerAndLogin(agent) + const { headers } = user || {} + + try { + await agent + .post('/tasks/create') + .send({ + url: 'https://github.com/worknenjoy/gitpay/issues/998', + provider: 'github', + private: true + }) + .set('Authorization', headers.authorization) + .expect('Content-Type', /json/) + .expect(200) + + // Assert that notifyNewIssue was not called + expect(slackStub.called).to.equal(false) + } finally { + slackStub.restore() + // Restore cache + delete require.cache[require.resolve('../../../src/modules/slack')] + delete require.cache[require.resolve('../../../src/modules/slack/index')] + delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] + delete require.cache[require.resolve('../../../src/modules/tasks')] + delete require.cache[require.resolve('../../../src/app/controllers/task')] + } + }) + + it('should call Slack methods when task is created as public', async () => { + // Clear require cache to ensure fresh module load with stub + delete require.cache[require.resolve('../../../src/modules/slack')] + delete require.cache[require.resolve('../../../src/modules/slack/index')] + delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] + delete require.cache[require.resolve('../../../src/modules/tasks')] + delete require.cache[require.resolve('../../../src/app/controllers/task')] + + // Setup GitHub API mocks + setupGitHubMocks(997) + + // Stub the Slack notification method + const SlackModule = require('../../../src/modules/slack') + const slackStub = sinon.stub(SlackModule, 'notifyNewIssue').resolves(true) + + const user = await registerAndLogin(agent) + const { headers, body: currentUser } = user || {} + + try { + await agent + .post('/tasks/create') + .send({ + url: 'https://github.com/worknenjoy/gitpay/issues/997', + provider: 'github' + }) + .set('Authorization', headers.authorization) + .expect('Content-Type', /json/) + .expect(200) + + // Assert that notifyNewIssue was called + expect(slackStub.calledOnce).to.equal(true) + + // Verify the call arguments + const [taskArg, userArg] = slackStub.firstCall.args + expect(taskArg).to.exist + expect(taskArg.id).to.exist + expect(userArg).to.exist + expect(userArg.id).to.equal(currentUser.id) + } finally { + slackStub.restore() + // Restore cache + delete require.cache[require.resolve('../../../src/modules/slack')] + delete require.cache[require.resolve('../../../src/modules/slack/index')] + delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] + delete require.cache[require.resolve('../../../src/modules/tasks')] + delete require.cache[require.resolve('../../../src/app/controllers/task')] + } + }) +}) diff --git a/test/task.test.js b/test/task.test.js index 53c52b358..0d5bc9961 100644 --- a/test/task.test.js +++ b/test/task.test.js @@ -222,117 +222,6 @@ describe('tasks', () => { .catch(done) }) - it('should not call Slack methods when task is created with not_listed set to true', async () => { - chai.use(spies) - const slackModule = require('../src/modules/slack') - const slackSpy = chai.spy.on(slackModule, 'notifyNewIssue') - const originalBotModule = require('../src/modules/bot/issueAddedComment') - const botSpy = chai.spy(originalBotModule) - - try { - const user = await registerAndLogin(agent) - const task = await models.Task.create({ - url: 'https://github.com/worknenjoy/gitpay/issues/999', - provider: 'github', - userId: user.body.id, - title: 'Test Task', - not_listed: true - }) - - const userData = await task.getUser() - const taskData = task.dataValues - - // Test the actual logic from taskBuilds.js - const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) - if (isTaskPublic) { - botSpy(task) - slackModule.notifyNewIssue(taskData, userData) - } - - expect(slackSpy).to.not.have.been.called() - expect(botSpy).to.not.have.been.called() - } finally { - chai.spy.restore(slackModule, 'notifyNewIssue') - } - }) - - it('should not call Slack methods when task is created with private set to true', async () => { - chai.use(spies) - const slackModule = require('../src/modules/slack') - const slackSpy = chai.spy.on(slackModule, 'notifyNewIssue') - const originalBotModule = require('../src/modules/bot/issueAddedComment') - const botSpy = chai.spy(originalBotModule) - - try { - const user = await registerAndLogin(agent) - const task = await models.Task.create({ - url: 'https://github.com/worknenjoy/gitpay/issues/998', - provider: 'github', - userId: user.body.id, - title: 'Test Task', - private: true - }) - - const userData = await task.getUser() - const taskData = task.dataValues - - // Test the actual logic from taskBuilds.js - const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) - if (isTaskPublic) { - botSpy(task) - slackModule.notifyNewIssue(taskData, userData) - } - - expect(slackSpy).to.not.have.been.called() - expect(botSpy).to.not.have.been.called() - } finally { - chai.spy.restore(slackModule, 'notifyNewIssue') - } - }) - - it('should call Slack methods when task is created as public', async () => { - chai.use(spies) - - // Set up spies first - const slackModule = require('../src/modules/slack') - const slackSpy = chai.spy.on(slackModule, 'notifyNewIssue') - - // For default export function, create a spy wrapper - delete require.cache[require.resolve('../src/modules/bot/issueAddedComment')] - const originalBotModule = require('../src/modules/bot/issueAddedComment') - const botSpy = chai.spy(originalBotModule) - - try { - const user = await registerAndLogin(agent) - // Create task without explicitly setting not_listed/private (should default to false) - const task = await models.Task.create({ - url: 'https://github.com/worknenjoy/gitpay/issues/997', - provider: 'github', - userId: user.body.id, - title: 'Test Task' - }) - - const userData = await task.getUser() - const taskData = task.dataValues - - // Test the actual logic from taskBuilds.js - const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) - if (isTaskPublic) { - // Use the spied version - botSpy(task) - slackModule.notifyNewIssue(taskData, userData) - } - - expect(slackSpy).to.have.been.called() - expect(botSpy).to.have.been.called() - } finally { - chai.spy.restore(slackModule, 'notifyNewIssue') - // Restore cache - delete require.cache[require.resolve('../src/modules/slack')] - delete require.cache[require.resolve('../src/modules/bot/issueAddedComment')] - } - }) - it('should give an error on create if the issue build responds with limit exceeded', (done) => { nockAuthLimitExceeded() registerAndLogin(agent) From 7ca142fe132e5a49a9600de1adc564696bfc4c91 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Fri, 16 Jan 2026 02:15:48 +0800 Subject: [PATCH 10/20] update test --- .../api/task/taskCreateAndPostToSlack.test.ts | 163 ++++++------------ 1 file changed, 54 insertions(+), 109 deletions(-) diff --git a/test/api/task/taskCreateAndPostToSlack.test.ts b/test/api/task/taskCreateAndPostToSlack.test.ts index 1ab746a5d..50f205beb 100644 --- a/test/api/task/taskCreateAndPostToSlack.test.ts +++ b/test/api/task/taskCreateAndPostToSlack.test.ts @@ -4,9 +4,6 @@ import request from 'supertest' import api from '../../../src/server' import { registerAndLogin, truncateModels } from '../../helpers' import Models from '../../../src/models' -// Use require to avoid TS type dependency on @types/sinon -// eslint-disable-next-line @typescript-eslint/no-var-requires -const sinon = require('sinon') const secrets = require('../../../src/config/secrets') const getSingleIssue = require('../../data/github/github.issue.get') const getSingleRepo = require('../../data/github/github.repository.get') @@ -50,141 +47,89 @@ describe('Task Creation and Slack Notifications', () => { afterEach(() => { nock.cleanAll() - sinon.restore() + delete process.env.SLACK_WEBHOOK_URL }) it('should not call Slack methods when task is created with not_listed set to true', async () => { - // Clear require cache to ensure fresh module load with stub - delete require.cache[require.resolve('../../../src/modules/slack')] - delete require.cache[require.resolve('../../../src/modules/slack/index')] - delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] - delete require.cache[require.resolve('../../../src/modules/tasks')] - delete require.cache[require.resolve('../../../src/app/controllers/task')] - // Setup GitHub API mocks setupGitHubMocks(999) - // Stub the Slack notification method - const SlackModule = require('../../../src/modules/slack') - const slackStub = sinon.stub(SlackModule, 'notifyNewIssue').resolves(true) + // Mock Slack webhook (following slack.test.js pattern) + process.env.SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/TEST/WEBHOOK/URL' + const slackWebhook = nock('https://hooks.slack.com') + .post('/services/TEST/WEBHOOK/URL') + .reply(200, 'ok') const user = await registerAndLogin(agent) const { headers } = user || {} - try { - await agent - .post('/tasks/create') - .send({ - url: 'https://github.com/worknenjoy/gitpay/issues/999', - provider: 'github', - not_listed: true - }) - .set('Authorization', headers.authorization) - .expect('Content-Type', /json/) - .expect(200) - - // Assert that notifyNewIssue was not called - expect(slackStub.called).to.equal(false) - } finally { - slackStub.restore() - // Restore cache - delete require.cache[require.resolve('../../../src/modules/slack')] - delete require.cache[require.resolve('../../../src/modules/slack/index')] - delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] - delete require.cache[require.resolve('../../../src/modules/tasks')] - delete require.cache[require.resolve('../../../src/app/controllers/task')] - } + await agent + .post('/tasks/create') + .send({ + url: 'https://github.com/worknenjoy/gitpay/issues/999', + provider: 'github', + not_listed: true + }) + .set('Authorization', headers.authorization) + .expect('Content-Type', /json/) + .expect(200) + + // Assert that Slack webhook was not called + expect(slackWebhook.isDone()).to.equal(false) }) it('should not call Slack methods when task is created with private set to true', async () => { - // Clear require cache to ensure fresh module load with stub - delete require.cache[require.resolve('../../../src/modules/slack')] - delete require.cache[require.resolve('../../../src/modules/slack/index')] - delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] - delete require.cache[require.resolve('../../../src/modules/tasks')] - delete require.cache[require.resolve('../../../src/app/controllers/task')] - // Setup GitHub API mocks setupGitHubMocks(998) - // Stub the Slack notification method - const SlackModule = require('../../../src/modules/slack') - const slackStub = sinon.stub(SlackModule, 'notifyNewIssue').resolves(true) + // Mock Slack webhook (following slack.test.js pattern) + process.env.SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/TEST/WEBHOOK/URL' + const slackWebhook = nock('https://hooks.slack.com') + .post('/services/TEST/WEBHOOK/URL') + .reply(200, 'ok') const user = await registerAndLogin(agent) const { headers } = user || {} - try { - await agent - .post('/tasks/create') - .send({ - url: 'https://github.com/worknenjoy/gitpay/issues/998', - provider: 'github', - private: true - }) - .set('Authorization', headers.authorization) - .expect('Content-Type', /json/) - .expect(200) - - // Assert that notifyNewIssue was not called - expect(slackStub.called).to.equal(false) - } finally { - slackStub.restore() - // Restore cache - delete require.cache[require.resolve('../../../src/modules/slack')] - delete require.cache[require.resolve('../../../src/modules/slack/index')] - delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] - delete require.cache[require.resolve('../../../src/modules/tasks')] - delete require.cache[require.resolve('../../../src/app/controllers/task')] - } + await agent + .post('/tasks/create') + .send({ + url: 'https://github.com/worknenjoy/gitpay/issues/998', + provider: 'github', + private: true + }) + .set('Authorization', headers.authorization) + .expect('Content-Type', /json/) + .expect(200) + + // Assert that Slack webhook was not called + expect(slackWebhook.isDone()).to.equal(false) }) it('should call Slack methods when task is created as public', async () => { - // Clear require cache to ensure fresh module load with stub - delete require.cache[require.resolve('../../../src/modules/slack')] - delete require.cache[require.resolve('../../../src/modules/slack/index')] - delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] - delete require.cache[require.resolve('../../../src/modules/tasks')] - delete require.cache[require.resolve('../../../src/app/controllers/task')] - // Setup GitHub API mocks setupGitHubMocks(997) - // Stub the Slack notification method - const SlackModule = require('../../../src/modules/slack') - const slackStub = sinon.stub(SlackModule, 'notifyNewIssue').resolves(true) + // Mock Slack webhook (following slack.test.js pattern) + process.env.SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/TEST/WEBHOOK/URL' + const slackWebhook = nock('https://hooks.slack.com') + .post('/services/TEST/WEBHOOK/URL') + .reply(200, 'ok') const user = await registerAndLogin(agent) const { headers, body: currentUser } = user || {} - try { - await agent - .post('/tasks/create') - .send({ - url: 'https://github.com/worknenjoy/gitpay/issues/997', - provider: 'github' - }) - .set('Authorization', headers.authorization) - .expect('Content-Type', /json/) - .expect(200) - - // Assert that notifyNewIssue was called - expect(slackStub.calledOnce).to.equal(true) - - // Verify the call arguments - const [taskArg, userArg] = slackStub.firstCall.args - expect(taskArg).to.exist - expect(taskArg.id).to.exist - expect(userArg).to.exist - expect(userArg.id).to.equal(currentUser.id) - } finally { - slackStub.restore() - // Restore cache - delete require.cache[require.resolve('../../../src/modules/slack')] - delete require.cache[require.resolve('../../../src/modules/slack/index')] - delete require.cache[require.resolve('../../../src/modules/tasks/taskBuilds')] - delete require.cache[require.resolve('../../../src/modules/tasks')] - delete require.cache[require.resolve('../../../src/app/controllers/task')] - } + await agent + .post('/tasks/create') + .send({ + url: 'https://github.com/worknenjoy/gitpay/issues/997', + provider: 'github' + }) + .set('Authorization', headers.authorization) + .expect('Content-Type', /json/) + .expect(200) + + // Assert that Slack webhook was called + expect(slackWebhook.isDone()).to.equal(true) }) }) From 06af7be9250495bd7923a4aec998dc92379e8adc Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Fri, 16 Jan 2026 20:37:31 +0800 Subject: [PATCH 11/20] remove unused import. --- test/task.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/task.test.js b/test/task.test.js index 0d5bc9961..d8dd79420 100644 --- a/test/task.test.js +++ b/test/task.test.js @@ -17,8 +17,6 @@ const spies = require('chai-spies') const AssignMail = require('../src/modules/mail/assign') const TaskMail = require('../src/modules/mail/task') const taskUpdate = require('../src/modules/tasks/taskUpdate') -const { notifyNewIssue } = require('../src/modules/slack') -const issueAddedComment = require('../src/modules/bot/issueAddedComment') const nockAuth = () => { nock('https://github.com') From 822b6087f31b0c887076d404efe4f2fa5e25f443 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Fri, 16 Jan 2026 23:55:20 +0800 Subject: [PATCH 12/20] Chore : update slack notification for new paid complete bounties --- src/modules/orders/orderAuthorize.js | 19 ++ src/modules/orders/orderBuilds.js | 38 +-- src/modules/orders/orderUpdateAfterStripe.js | 23 +- src/modules/slack/index.js | 3 +- src/modules/slack/index.ts | 166 ++++++++++++++ src/modules/slack/types.ts | 45 ++++ src/modules/webhooks/chargeSucceeded.js | 41 +++- src/modules/webhooks/chargeUpdated.js | 39 +++- .../webhooks/invoicePaymentSucceeded.js | 35 ++- src/modules/webhooks/invoiceUpdated.js | 28 ++- test/order.test.js | 216 ++++++++++++++++-- 11 files changed, 606 insertions(+), 47 deletions(-) create mode 100644 src/modules/slack/index.ts create mode 100644 src/modules/slack/types.ts diff --git a/src/modules/orders/orderAuthorize.js b/src/modules/orders/orderAuthorize.js index dc63d7752..40d7983e5 100644 --- a/src/modules/orders/orderAuthorize.js +++ b/src/modules/orders/orderAuthorize.js @@ -3,6 +3,7 @@ const Promise = require('bluebird') const requestPromise = require('request-promise') const models = require('../../models') const comment = require('../bot/comment') +const { notifyNewBounty } = require('../slack') module.exports = Promise.method(function orderAuthorize(orderParameters) { return requestPromise({ @@ -63,6 +64,24 @@ module.exports = Promise.method(function orderAuthorize(orderParameters) { if (orderData.paid) { comment(orderData, task) PaymentMail.success(user, task, orderData.amount) + + // Send Slack notification for PayPal payment completion + const shouldNotifySlack = + task && + user && + !(task.dataValues.not_listed === true || task.dataValues.private === true) + + if (shouldNotifySlack) { + const orderDataForNotification = { + amount: orderData.amount, + currency: orderData.currency || 'USD' + } + notifyNewBounty(task.dataValues, orderDataForNotification, user.dataValues).catch( + (e) => { + console.log('error on send slack notification for new bounty', e) + } + ) + } } else { PaymentMail.error(user.dataValues, task, orderData.amount) } diff --git a/src/modules/orders/orderBuilds.js b/src/modules/orders/orderBuilds.js index 51205caa9..36e0debc4 100644 --- a/src/modules/orders/orderBuilds.js +++ b/src/modules/orders/orderBuilds.js @@ -64,23 +64,6 @@ module.exports = async function orderBuilds(orderParameters) { const taskTitle = orderCreated?.Task?.dataValues?.title || '' const percentage = orderCreated.Plan?.feePercentage - // Skip Slack notifications for private or not_listed tasks - const shouldNotifySlack = - orderCreated.Task && - orderCreated.User && - !( - orderCreated.Task.dataValues.not_listed === true || - orderCreated.Task.dataValues.private === true - ) - - if (shouldNotifySlack) { - notifyNewBounty( - orderCreated.Task.dataValues, - orderCreated.dataValues, - orderCreated.User.dataValues - ) - } - if (orderParameters.provider === 'stripe' && orderParameters.source_type === 'invoice-item') { const unitAmount = (parseInt(orderParameters.amount) * 100 * (1 + percentage / 100)).toFixed(0) const quantity = 1 @@ -247,6 +230,27 @@ module.exports = async function orderBuilds(orderParameters) { } ) + // Send Slack notification for wallet payment (paid immediately) + const shouldNotifySlack = + orderCreated.Task && + orderCreated.User && + !( + orderCreated.Task.dataValues.not_listed === true || + orderCreated.Task.dataValues.private === true + ) + + if (shouldNotifySlack) { + try { + const orderData = { + amount: orderCreated.dataValues.amount, + currency: orderCreated.dataValues.currency || 'USD' + } + await notifyNewBounty(orderCreated.Task.dataValues, orderData, orderCreated.User.dataValues) + } catch (e) { + console.log('error on send slack notification for new bounty', e) + } + } + return orderUpdated } diff --git a/src/modules/orders/orderUpdateAfterStripe.js b/src/modules/orders/orderUpdateAfterStripe.js index bbef5d066..e440a8f17 100644 --- a/src/modules/orders/orderUpdateAfterStripe.js +++ b/src/modules/orders/orderUpdateAfterStripe.js @@ -30,14 +30,21 @@ module.exports = Promise.method( // Send Slack notification for new bounty payment if (orderPayload.paid && orderPayload.status === 'succeeded') { - const orderData = { - amount: order.amount || orderParameters.amount, - currency: order.currency || orderParameters.currency || 'USD' - } - try { - await notifyNewBounty(task.dataValues, orderData, user) - } catch (e) { - console.log('error on send slack notification for new bounty', e) + const shouldNotifySlack = + task && + user && + !(task.dataValues.not_listed === true || task.dataValues.private === true) + + if (shouldNotifySlack) { + const orderData = { + amount: order.amount || orderParameters.amount, + currency: order.currency || orderParameters.currency || 'USD' + } + try { + await notifyNewBounty(task.dataValues, orderData, user) + } catch (e) { + console.log('error on send slack notification for new bounty', e) + } } } diff --git a/src/modules/slack/index.js b/src/modules/slack/index.js index e1996d930..4939e55d2 100644 --- a/src/modules/slack/index.js +++ b/src/modules/slack/index.js @@ -15,7 +15,8 @@ const sendSlackMessage = async (payload) => { }) return true } catch (error) { - console.error('Slack notification failed:', error.message) + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + console.error('Slack notification failed:', errorMessage) return false } } diff --git a/src/modules/slack/index.ts b/src/modules/slack/index.ts new file mode 100644 index 000000000..ebba615af --- /dev/null +++ b/src/modules/slack/index.ts @@ -0,0 +1,166 @@ +/** + * Slack notification module + * Handles sending notifications to Slack channel for new issues and bounties + */ + +import * as requestPromise from 'request-promise' +import secrets from '../../config/secrets' +import type { Task, User, OrderData, SlackMessagePayload } from './types' + +const sendSlackMessage = async (payload: SlackMessagePayload): Promise => { + const webhookUrl = process.env.SLACK_WEBHOOK_URL || secrets.slack?.webhookUrl + + if (!webhookUrl) { + return false + } + + try { + await requestPromise({ + method: 'POST', + uri: webhookUrl, + body: payload, + json: true + }) + return true + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + console.error('Slack notification failed:', errorMessage) + return false + } +} + +const formatCurrency = (amount: number | string, currency: string = 'USD'): string => { + const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount + + if (isNaN(numAmount)) { + return '$0.00' + } + + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency + }).format(numAmount) +} + +/** + * Sends a Slack notification when a new issue is imported + * @param task - The task/issue that was imported + * @param user - The user who imported the issue + * @returns Promise - True if notification was sent successfully + */ +export const notifyNewIssue = async ( + task: Task | null | undefined, + user: User | null | undefined +): Promise => { + if (!task?.id) { + return false + } + + const username = user?.username || user?.name || 'Unknown' + + return await sendSlackMessage({ + username: 'Gitpay', + blocks: [ + { + type: 'header', + text: { + type: 'plain_text', + text: 'New issue imported' + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*${task.title || 'Untitled Issue'}*` + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: task.description || 'No description provided.' + } + } + ], + attachments: [ + { + color: '#047651', + blocks: [ + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `Imported by *${username}*` + } + ] + } + ] + } + ] + }) +} + +/** + * Sends a Slack notification when a new bounty payment is completed + * @param task - The task/bounty that received payment + * @param order - The order data containing amount and currency + * @param user - The user who made the payment + * @returns Promise - True if notification was sent successfully + */ +export const notifyNewBounty = async ( + task: Task | null | undefined, + order: OrderData | null | undefined, + user: User | null | undefined +): Promise => { + if (!task?.id || !order?.amount) { + return false + } + + const username = user?.username || user?.name || 'Unknown' + const amount = formatCurrency(order.amount, order.currency) + + return await sendSlackMessage({ + username: 'Gitpay', + blocks: [ + { + type: 'header', + text: { + type: 'plain_text', + text: 'New bounty added' + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*${amount}*` + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*${task.title || 'Untitled Task'}*` + } + } + ], + attachments: [ + { + color: '#047651', + blocks: [ + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `Bounty set by *${username}*` + } + ] + } + ] + } + ] + }) +} diff --git a/src/modules/slack/types.ts b/src/modules/slack/types.ts new file mode 100644 index 000000000..87825342c --- /dev/null +++ b/src/modules/slack/types.ts @@ -0,0 +1,45 @@ +/** + * Type definitions for Slack notification module + */ + +export interface Task { + id?: number + title?: string + description?: string + not_listed?: boolean + private?: boolean +} + +export interface User { + id?: number + username?: string + name?: string +} + +export interface OrderData { + amount: number | string + currency?: string +} + +export interface SlackMessagePayload { + username: string + blocks: SlackBlock[] + attachments?: SlackAttachment[] +} + +export interface SlackBlock { + type: string + text?: { + type: string + text: string + } + elements?: Array<{ + type: string + text: string + }> +} + +export interface SlackAttachment { + color: string + blocks: SlackBlock[] +} diff --git a/src/modules/webhooks/chargeSucceeded.js b/src/modules/webhooks/chargeSucceeded.js index 6812dfab3..2461a6b1c 100644 --- a/src/modules/webhooks/chargeSucceeded.js +++ b/src/modules/webhooks/chargeSucceeded.js @@ -1,6 +1,7 @@ const models = require('../../models') const i18n = require('i18n') const SendMail = require('../mail/mail') +const { notifyNewBounty } = require('../slack') const sendEmailSuccess = (event, paid, status, order, req, res) => { return models.User.findOne({ @@ -30,7 +31,7 @@ const sendEmailSuccess = (event, paid, status, order, req, res) => { }) } -const updateOrder = (event, paid, status, req, res) => { +const updateOrder = async (event, paid, status, req, res) => { return models.Order.update( { paid: paid, @@ -44,8 +45,44 @@ const updateOrder = (event, paid, status, req, res) => { returning: true } ) - .then((order) => { + .then(async (order) => { if (order[0]) { + // Send Slack notification if payment succeeded + if (paid && status === 'succeeded') { + const orderUpdated = await models.Order.findOne({ + where: { + id: order[1][0].dataValues.id + }, + include: [models.Task, models.User] + }) + + if (orderUpdated) { + const shouldNotifySlack = + orderUpdated.Task && + orderUpdated.User && + !( + orderUpdated.Task.dataValues.not_listed === true || + orderUpdated.Task.dataValues.private === true + ) + + if (shouldNotifySlack) { + const orderData = { + amount: orderUpdated.amount, + currency: orderUpdated.currency || 'USD' + } + try { + await notifyNewBounty( + orderUpdated.Task.dataValues, + orderData, + orderUpdated.User.dataValues + ) + } catch (e) { + console.log('error on send slack notification for new bounty', e) + } + } + } + } + return sendEmailSuccess(event, paid, status, order, req, res) } }) diff --git a/src/modules/webhooks/chargeUpdated.js b/src/modules/webhooks/chargeUpdated.js index 88d0299a4..c7fb5a4e2 100644 --- a/src/modules/webhooks/chargeUpdated.js +++ b/src/modules/webhooks/chargeUpdated.js @@ -2,6 +2,7 @@ const models = require('../../models') const i18n = require('i18n') const moment = require('moment') const SendMail = require('../mail/mail') +const { notifyNewBounty } = require('../slack') module.exports = async function chargeUpdated(event, paid, status, req, res) { if (event?.data?.object?.source?.id) { @@ -18,14 +19,14 @@ module.exports = async function chargeUpdated(event, paid, status, req, res) { returning: true } ) - .then((order) => { + .then(async (order) => { if (order[0]) { return models.User.findOne({ where: { id: order[1][0].dataValues.userId } }) - .then((user) => { + .then(async (user) => { if (user) { if (paid && status === 'succeeded') { const language = user.language || 'en' @@ -37,6 +38,40 @@ module.exports = async function chargeUpdated(event, paid, status, req, res) { amount: event.data.object.amount / 100 }) ) + + // Send Slack notification for charge update payment completion + const orderUpdated = await models.Order.findOne({ + where: { + id: order[1][0].dataValues.id + }, + include: [models.Task, models.User] + }) + + if (orderUpdated) { + const shouldNotifySlack = + orderUpdated.Task && + orderUpdated.User && + !( + orderUpdated.Task.dataValues.not_listed === true || + orderUpdated.Task.dataValues.private === true + ) + + if (shouldNotifySlack) { + const orderData = { + amount: orderUpdated.amount, + currency: orderUpdated.currency || 'USD' + } + try { + await notifyNewBounty( + orderUpdated.Task.dataValues, + orderData, + orderUpdated.User.dataValues + ) + } catch (e) { + console.log('error on send slack notification for new bounty', e) + } + } + } } } return res.status(200).json(event) diff --git a/src/modules/webhooks/invoicePaymentSucceeded.js b/src/modules/webhooks/invoicePaymentSucceeded.js index 6665e29e7..2a1dd5ea1 100644 --- a/src/modules/webhooks/invoicePaymentSucceeded.js +++ b/src/modules/webhooks/invoicePaymentSucceeded.js @@ -5,6 +5,7 @@ const SendMail = require('../mail/mail') const WalletMail = require('../mail/wallet') const stripe = require('../shared/stripe/stripe')() const { FAILED_REASON, CURRENCIES, formatStripeAmount } = require('./constants') +const { notifyNewBounty } = require('../slack') module.exports = async function invoicePaymentSucceeded(event, req, res) { return models.User.findOne({ @@ -35,7 +36,39 @@ module.exports = async function invoicePaymentSucceeded(event, req, res) { }, returning: true } - ).then((order) => { + ).then(async (order) => { + // Send Slack notification for invoice payment completion + if (order[0] && order[1].length) { + const orderUpdated = await models.Order.findOne({ + where: { + id: order[1][0].dataValues.id + }, + include: [models.Task, models.User] + }) + + if (orderUpdated && orderUpdated.Task && orderUpdated.User) { + const shouldNotifySlack = !( + orderUpdated.Task.dataValues.not_listed === true || + orderUpdated.Task.dataValues.private === true + ) + + if (shouldNotifySlack) { + const orderData = { + amount: orderUpdated.amount, + currency: orderUpdated.currency || 'USD' + } + try { + await notifyNewBounty( + orderUpdated.Task.dataValues, + orderData, + orderUpdated.User.dataValues + ) + } catch (e) { + console.log('error on send slack notification for new bounty', e) + } + } + } + } return res.status(200).json(event) }) } diff --git a/src/modules/webhooks/invoiceUpdated.js b/src/modules/webhooks/invoiceUpdated.js index 9d0a0eb84..a2748ade7 100644 --- a/src/modules/webhooks/invoiceUpdated.js +++ b/src/modules/webhooks/invoiceUpdated.js @@ -5,6 +5,7 @@ const SendMail = require('../mail/mail') const WalletMail = require('../mail/wallet') const stripe = require('../shared/stripe/stripe')() const { FAILED_REASON, CURRENCIES, formatStripeAmount } = require('./constants') +const { notifyNewBounty } = require('../slack') module.exports = async function invoiceUpdated(event, req, res) { // eslint-disable-next-line no-case-declarations @@ -62,7 +63,7 @@ module.exports = async function invoiceUpdated(event, req, res) { const userAssigned = userAssign.dataValues.User.dataValues const userTask = orderUpdated.User.dataValues if (orderUpdated) { - if (orderUpdated.status === 'paid') { + if (orderUpdated.status === 'paid' && orderUpdated.paid) { const userAssignedlanguage = userAssigned.language || 'en' i18n.setLocale(userAssignedlanguage) SendMail.success( @@ -81,6 +82,31 @@ module.exports = async function invoiceUpdated(event, req, res) { amount: order[1][0].dataValues.amount }) ) + + // Send Slack notification for invoice payment completion + const shouldNotifySlack = + orderUpdated.Task && + orderUpdated.User && + !( + orderUpdated.Task.dataValues.not_listed === true || + orderUpdated.Task.dataValues.private === true + ) + + if (shouldNotifySlack) { + const orderData = { + amount: orderUpdated.amount, + currency: orderUpdated.currency || 'USD' + } + try { + await notifyNewBounty( + orderUpdated.Task.dataValues, + orderData, + orderUpdated.User.dataValues + ) + } catch (e) { + console.log('error on send slack notification for new bounty', e) + } + } } } return res.status(200).json(event) diff --git a/test/order.test.js b/test/order.test.js index 9f51ed109..24458f4be 100644 --- a/test/order.test.js +++ b/test/order.test.js @@ -125,24 +125,14 @@ describe('Orders', () => { } }) - it('should call notifyNewBounty when order is created for a public task', async () => { + it('should not call notifyNewBounty when order is created (notification only on payment completion)', async () => { chai.use(spies) - - // Clear require cache first - delete require.cache[require.resolve('../src/modules/slack')] - delete require.cache[require.resolve('../src/modules/orders/orderBuilds')] - delete require.cache[require.resolve('../src/modules/orders')] - - // Now set up spy on the fresh module const slackModule = require('../src/modules/slack') const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') - - // Re-require orderBuilds so it picks up the spied version const orderBuilds = require('../src/modules/orders').orderBuilds try { const user = await registerAndLogin(agent) - // Create task with explicit false values to ensure they're set correctly const task = await models.Task.create({ url: 'https://github.com/test/repo/issues/3', userId: user.body.id, @@ -160,13 +150,90 @@ describe('Orders', () => { taskId: task.id }) + // Notification should NOT be called when order is created, only when payment completes + expect(slackSpy).to.not.have.been.called() + } finally { + chai.spy.restore(slackModule, 'notifyNewBounty') + } + }) + + it('should call notifyNewBounty when wallet payment completes for a public task', async () => { + chai.use(spies) + const slackModule = require('../src/modules/slack') + const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + const orderBuilds = require('../src/modules/orders').orderBuilds + + try { + const user = await registerAndLogin(agent) + const wallet = await models.Wallet.create({ + name: 'Test Wallet', + balance: 500, + userId: user.body.id + }) + const task = await models.Task.create({ + url: 'https://github.com/test/repo/issues/4', + userId: user.body.id, + title: 'Test Task', + not_listed: false, + private: false + }) + + await orderBuilds({ + walletId: wallet.id, + currency: 'USD', + amount: 200, + provider: 'wallet', + source_type: 'wallet-funds', + userId: user.body.id, + taskId: task.id + }) + + // Notification should be called when wallet payment completes expect(slackSpy).to.have.been.called() + expect(slackSpy).to.have.been.called.with( + task.dataValues, + { amount: 200, currency: 'USD' }, + user.body + ) + } finally { + chai.spy.restore(slackModule, 'notifyNewBounty') + } + }) + + it('should not call notifyNewBounty when wallet payment completes for a private task', async () => { + chai.use(spies) + const slackModule = require('../src/modules/slack') + const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + const orderBuilds = require('../src/modules/orders').orderBuilds + + try { + const user = await registerAndLogin(agent) + const wallet = await models.Wallet.create({ + name: 'Test Wallet', + balance: 500, + userId: user.body.id + }) + const task = await models.Task.create({ + url: 'https://github.com/test/repo/issues/5', + userId: user.body.id, + title: 'Test Task', + private: true + }) + + await orderBuilds({ + walletId: wallet.id, + currency: 'USD', + amount: 200, + provider: 'wallet', + source_type: 'wallet-funds', + userId: user.body.id, + taskId: task.id + }) + + // Notification should NOT be called for private tasks + expect(slackSpy).to.not.have.been.called() } finally { chai.spy.restore(slackModule, 'notifyNewBounty') - // Restore cache - delete require.cache[require.resolve('../src/modules/slack')] - delete require.cache[require.resolve('../src/modules/orders/orderBuilds')] - delete require.cache[require.resolve('../src/modules/orders')] } }) @@ -838,6 +905,125 @@ describe('Orders', () => { expect(transferRes.body.currency).to.equal('BRL') expect(transferRes.body.amount).to.equal('200') }) + describe('PayPal payment notifications', () => { + it('should call notifyNewBounty when PayPal payment completes for a public task', async () => { + chai.use(spies) + const slackModule = require('../src/modules/slack') + const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + const orderAuthorize = require('../src/modules/orders').orderAuthorize + + // Mock PayPal API responses + nock(process.env.PAYPAL_HOST || 'https://api.sandbox.paypal.com') + .post('/v1/oauth2/token') + .reply(200, JSON.stringify({ access_token: 'test_token' })) + + nock(process.env.PAYPAL_HOST || 'https://api.sandbox.paypal.com') + .post(/\/v2\/checkout\/orders\/.*\/authorize/) + .reply(200, JSON.stringify({ + id: 'TEST_ORDER_ID', + purchase_units: [{ + payments: { + authorizations: [{ + id: 'TEST_AUTH_ID' + }] + } + }] + })) + + try { + const user = await registerAndLogin(agent) + const task = await models.Task.create({ + url: 'https://github.com/test/repo/issues/6', + userId: user.body.id, + title: 'Test Task', + not_listed: false, + private: false + }) + + const order = await models.Order.create({ + source_id: 'TEST_ORDER_ID', + currency: 'USD', + amount: 200, + provider: 'paypal', + userId: user.body.id, + TaskId: task.id, + token: 'TEST_TOKEN', + status: 'open', + paid: false + }) + + await orderAuthorize({ + token: 'TEST_TOKEN', + PayerID: 'TEST_PAYER_ID' + }) + + // Notification should be called when PayPal payment completes + expect(slackSpy).to.have.been.called() + } finally { + chai.spy.restore(slackModule, 'notifyNewBounty') + nock.cleanAll() + } + }) + + it('should not call notifyNewBounty when PayPal payment completes for a private task', async () => { + chai.use(spies) + const slackModule = require('../src/modules/slack') + const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + const orderAuthorize = require('../src/modules/orders').orderAuthorize + + // Mock PayPal API responses + nock(process.env.PAYPAL_HOST || 'https://api.sandbox.paypal.com') + .post('/v1/oauth2/token') + .reply(200, JSON.stringify({ access_token: 'test_token' })) + + nock(process.env.PAYPAL_HOST || 'https://api.sandbox.paypal.com') + .post(/\/v2\/checkout\/orders\/.*\/authorize/) + .reply(200, JSON.stringify({ + id: 'TEST_ORDER_ID', + purchase_units: [{ + payments: { + authorizations: [{ + id: 'TEST_AUTH_ID' + }] + } + }] + })) + + try { + const user = await registerAndLogin(agent) + const task = await models.Task.create({ + url: 'https://github.com/test/repo/issues/7', + userId: user.body.id, + title: 'Test Task', + private: true + }) + + const order = await models.Order.create({ + source_id: 'TEST_ORDER_ID', + currency: 'USD', + amount: 200, + provider: 'paypal', + userId: user.body.id, + TaskId: task.id, + token: 'TEST_TOKEN_2', + status: 'open', + paid: false + }) + + await orderAuthorize({ + token: 'TEST_TOKEN_2', + PayerID: 'TEST_PAYER_ID' + }) + + // Notification should NOT be called for private tasks + expect(slackSpy).to.not.have.been.called() + } finally { + chai.spy.restore(slackModule, 'notifyNewBounty') + nock.cleanAll() + } + }) + }) + it('should refund order', async () => { const stripeRefund = { id: 're_1J2Yal2eZvKYlo2C0qvW9j8D', From 5af29c30a861d334c442a1bba1af9ca0e538a98f Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Sat, 17 Jan 2026 05:30:34 +0800 Subject: [PATCH 13/20] fix props error --- .../issue-actions-by-role.tsx | 28 +++++---- .../issue-payment-drawer.tsx | 8 ++- .../sections/issue-sidebar/issue-sidebar.tsx | 4 ++ .../credit-card-payment-form.tsx | 18 +++++- .../issue-page-layout/issue-page-layout.tsx | 6 +- .../issue-private-page/issue-private-page.tsx | 6 +- .../issue-public-page/issue-public-page.tsx | 6 +- frontend/src/containers/task-private.js | 7 ++- frontend/src/containers/task.js | 5 +- src/modules/orders/orderAuthorize.js | 28 ++++----- src/modules/orders/orderBuilds.js | 38 ++++++------ src/modules/orders/orderUpdateAfterStripe.js | 23 ++----- src/modules/slack/index.js | 56 ++++++++++++++++- src/modules/slack/index.ts | 61 ++++++++++++++++++- src/modules/webhooks/chargeSucceeded.js | 33 +++------- src/modules/webhooks/chargeUpdated.js | 33 +++------- .../webhooks/invoicePaymentSucceeded.js | 30 +++------ src/modules/webhooks/invoiceUpdated.js | 33 +++------- test/order.test.js | 11 +++- 19 files changed, 267 insertions(+), 167 deletions(-) diff --git a/frontend/src/components/design-library/atoms/buttons/issue-actions-by-role/issue-actions-by-role.tsx b/frontend/src/components/design-library/atoms/buttons/issue-actions-by-role/issue-actions-by-role.tsx index ede07d5b3..f1aafd4b4 100644 --- a/frontend/src/components/design-library/atoms/buttons/issue-actions-by-role/issue-actions-by-role.tsx +++ b/frontend/src/components/design-library/atoms/buttons/issue-actions-by-role/issue-actions-by-role.tsx @@ -33,6 +33,8 @@ interface IssueActionsProps { fetchTask: (taskId: number) => void syncTask: (taskId: number) => void taskSolutionCompleted: boolean + validateCoupon?: (code: string, originalOrderPrice: number) => void + couponStoreState?: any } const IssueActionsByRole = ({ @@ -51,17 +53,19 @@ const IssueActionsByRole = ({ fetchPullRequestData, fetchCustomer, customer, - addNotification, - updateTask, - task, - createOrder, - order, - fetchWallet, - wallet, - listWallets, - wallets, - fetchTask, - syncTask + addNotification, + updateTask, + task, + createOrder, + order, + fetchWallet, + wallet, + listWallets, + wallets, + fetchTask, + syncTask, + validateCoupon, + couponStoreState }: IssueActionsProps) => { const { data } = issue @@ -98,6 +102,8 @@ const IssueActionsByRole = ({ fetchTask={fetchTask} syncTask={syncTask} price={data?.price || 0} + validateCoupon={validateCoupon} + couponStoreState={couponStoreState} /> ) }, diff --git a/frontend/src/components/design-library/molecules/drawers/issue-payment-drawer/issue-payment-drawer.tsx b/frontend/src/components/design-library/molecules/drawers/issue-payment-drawer/issue-payment-drawer.tsx index 4adb39d34..298de9e63 100644 --- a/frontend/src/components/design-library/molecules/drawers/issue-payment-drawer/issue-payment-drawer.tsx +++ b/frontend/src/components/design-library/molecules/drawers/issue-payment-drawer/issue-payment-drawer.tsx @@ -1,5 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react' import { FormattedMessage, defineMessages, useIntl } from 'react-intl' +import { connect } from 'react-redux' +import { validateCoupon } from '../../../../../actions/couponActions' import PaymentDrawer from 'design-library/molecules/drawers/payment-drawer/payment-drawer' @@ -45,7 +47,9 @@ function IssuePaymentDrawer({ listWallets, wallets, fetchTask, - syncTask + syncTask, + validateCoupon, + couponStoreState }: any) { const intl = useIntl() @@ -152,6 +156,8 @@ function IssuePaymentDrawer({ task={task?.data} plan={plan} onClose={onClose} + validateCoupon={validateCoupon} + couponStoreState={couponStoreState} /> ) }, diff --git a/frontend/src/components/design-library/molecules/sections/issue-sidebar/issue-sidebar.tsx b/frontend/src/components/design-library/molecules/sections/issue-sidebar/issue-sidebar.tsx index d928960a2..2ae92eba6 100644 --- a/frontend/src/components/design-library/molecules/sections/issue-sidebar/issue-sidebar.tsx +++ b/frontend/src/components/design-library/molecules/sections/issue-sidebar/issue-sidebar.tsx @@ -51,6 +51,8 @@ const IssueSidebar = ({ wallets, fetchTask, syncTask, + validateCoupon, + couponStoreState, fetchCustomer }) => { const intl = useIntl() @@ -294,6 +296,8 @@ const IssueSidebar = ({ wallets={wallets} fetchTask={fetchTask} syncTask={syncTask} + validateCoupon={validateCoupon} + couponStoreState={couponStoreState} /> { } if (!stripe || !elements) { - props.addNotification('payment.message.error', 'Stripe not initialized') + if (props.addNotification && typeof props.addNotification === 'function') { + props.addNotification('payment.message.error', 'Stripe not initialized') + } else { + console.error('Stripe not initialized and addNotification is not available') + } setCheckoutFormState((prev) => ({ ...prev, paymentRequested: false })) return } @@ -95,7 +99,11 @@ const CheckoutForm = (props) => { } } catch (e) { console.log('Error creating token or processing payment:', e) - props.addNotification('payment.message.error', e.message || 'Error processing payment') + if (props.addNotification && typeof props.addNotification === 'function') { + props.addNotification('payment.message.error', e.message || 'Error processing payment') + } else { + console.error('Payment error:', e.message || 'Error processing payment') + } setCheckoutFormState((prev) => ({ ...prev, paymentRequested: false })) } } @@ -148,7 +156,11 @@ const CheckoutForm = (props) => { } const applyCoupon = () => { - props.validateCoupon(couponState.coupon, price) + if (props.validateCoupon && typeof props.validateCoupon === 'function') { + props.validateCoupon(couponState.coupon, price) + } else { + console.error('validateCoupon is not available') + } } const logged = checkoutFormState.authenticated diff --git a/frontend/src/components/design-library/organisms/layouts/page-layouts/issue-page-layout/issue-page-layout.tsx b/frontend/src/components/design-library/organisms/layouts/page-layouts/issue-page-layout/issue-page-layout.tsx index 46b463e50..a3d1bc4f8 100644 --- a/frontend/src/components/design-library/organisms/layouts/page-layouts/issue-page-layout/issue-page-layout.tsx +++ b/frontend/src/components/design-library/organisms/layouts/page-layouts/issue-page-layout/issue-page-layout.tsx @@ -32,7 +32,9 @@ const IssuePage = ({ listWallets, wallets, fetchTask, - syncTask + syncTask, + validateCoupon, + couponStoreState }) => { return ( @@ -74,6 +76,8 @@ const IssuePage = ({ fetchTask={fetchTask} syncTask={syncTask} fetchCustomer={fetchCustomer} + validateCoupon={validateCoupon} + couponStoreState={couponStoreState} /> diff --git a/frontend/src/components/design-library/pages/private-pages/issue-pages/issue-private-page/issue-private-page.tsx b/frontend/src/components/design-library/pages/private-pages/issue-pages/issue-private-page/issue-private-page.tsx index 760790c0a..da8bd61ef 100644 --- a/frontend/src/components/design-library/pages/private-pages/issue-pages/issue-private-page/issue-private-page.tsx +++ b/frontend/src/components/design-library/pages/private-pages/issue-pages/issue-private-page/issue-private-page.tsx @@ -30,7 +30,9 @@ const IssuePrivatePage = ({ listWallets, wallets, fetchTask, - syncTask + syncTask, + validateCoupon, + couponStoreState }) => { return ( ) } diff --git a/frontend/src/components/design-library/pages/public-pages/issue-public-page/issue-public-page.tsx b/frontend/src/components/design-library/pages/public-pages/issue-public-page/issue-public-page.tsx index 34f1fc75d..cdb066cc3 100644 --- a/frontend/src/components/design-library/pages/public-pages/issue-public-page/issue-public-page.tsx +++ b/frontend/src/components/design-library/pages/public-pages/issue-public-page/issue-public-page.tsx @@ -30,7 +30,9 @@ const IssuePublicPage = ({ listWallets, wallets, fetchTask, - syncTask + syncTask, + validateCoupon, + couponStoreState }) => { return ( ) } diff --git a/frontend/src/containers/task-private.js b/frontend/src/containers/task-private.js index aadc507b3..faf87ecd7 100644 --- a/frontend/src/containers/task-private.js +++ b/frontend/src/containers/task-private.js @@ -34,6 +34,7 @@ import { detailOrder, listOrders } from '../actions/orderActions' +import { validateCoupon } from '../actions/couponActions' import { fetchWallet, listWallets } from '../actions/walletActions' import { getTaskSolution, @@ -62,7 +63,8 @@ const mapStateToProps = (state, ownProps) => { order: state.order, customer: state.customer, wallets: state.wallets, - wallet: state.wallet + wallet: state.wallet, + couponStoreState: { ...state.couponReducer } } } @@ -115,7 +117,8 @@ const mapDispatchToProps = (dispatch, ownProps) => { fetchPullRequestData: (owner, repositoryName, pullRequestId, taskId) => dispatch(fetchPullRequestData(owner, repositoryName, pullRequestId, taskId)), cleanPullRequestDataState: () => dispatch(cleanPullRequestDataState()), - fetchAccount: () => dispatch(fetchAccount()) + fetchAccount: () => dispatch(fetchAccount()), + validateCoupon: (code, originalOrderPrice) => dispatch(validateCoupon(code, originalOrderPrice)) // For account menu and bottom bar props // signOut and getInfo provided by profile container } diff --git a/frontend/src/containers/task.js b/frontend/src/containers/task.js index 4693ed045..d02664c06 100644 --- a/frontend/src/containers/task.js +++ b/frontend/src/containers/task.js @@ -37,6 +37,7 @@ import { detailOrder, listOrders } from '../actions/orderActions' +import { validateCoupon } from '../actions/couponActions' import { fetchWallet, listWallets } from '../actions/walletActions' import { getTaskSolution, @@ -69,7 +70,8 @@ const mapStateToProps = (state, ownProps) => { order: state.order, customer: state.customer, wallets: state.wallets, - wallet: state.wallet + wallet: state.wallet, + couponStoreState: { ...state.couponReducer } } } @@ -125,6 +127,7 @@ const mapDispatchToProps = (dispatch, ownProps) => { dispatch(fetchPullRequestData(owner, repositoryName, pullRequestId, taskId)), cleanPullRequestDataState: () => dispatch(cleanPullRequestDataState()), fetchAccount: () => dispatch(fetchAccount()), + validateCoupon: (code, originalOrderPrice) => dispatch(validateCoupon(code, originalOrderPrice)), // For account menu and bottom bar props signOut: () => dispatch(logOut()), getInfo: () => dispatch(getInfoAction()) diff --git a/src/modules/orders/orderAuthorize.js b/src/modules/orders/orderAuthorize.js index 40d7983e5..c833cf955 100644 --- a/src/modules/orders/orderAuthorize.js +++ b/src/modules/orders/orderAuthorize.js @@ -3,7 +3,7 @@ const Promise = require('bluebird') const requestPromise = require('request-promise') const models = require('../../models') const comment = require('../bot/comment') -const { notifyNewBounty } = require('../slack') +const { notifyBountyWithErrorHandling } = require('../slack') module.exports = Promise.method(function orderAuthorize(orderParameters) { return requestPromise({ @@ -60,28 +60,22 @@ module.exports = Promise.method(function orderAuthorize(orderParameters) { return Promise.all([ models.User.findByPk(orderData.userId), models.Task.findByPk(orderData.TaskId) - ]).spread((user, task) => { + ]).spread(async (user, task) => { if (orderData.paid) { comment(orderData, task) PaymentMail.success(user, task, orderData.amount) // Send Slack notification for PayPal payment completion - const shouldNotifySlack = - task && - user && - !(task.dataValues.not_listed === true || task.dataValues.private === true) - - if (shouldNotifySlack) { - const orderDataForNotification = { - amount: orderData.amount, - currency: orderData.currency || 'USD' - } - notifyNewBounty(task.dataValues, orderDataForNotification, user.dataValues).catch( - (e) => { - console.log('error on send slack notification for new bounty', e) - } - ) + const orderDataForNotification = { + amount: orderData.amount, + currency: orderData.currency || 'USD' } + await notifyBountyWithErrorHandling( + task, + orderDataForNotification, + user, + 'PayPal payment' + ) } else { PaymentMail.error(user.dataValues, task, orderData.amount) } diff --git a/src/modules/orders/orderBuilds.js b/src/modules/orders/orderBuilds.js index 36e0debc4..7d25576ae 100644 --- a/src/modules/orders/orderBuilds.js +++ b/src/modules/orders/orderBuilds.js @@ -1,4 +1,3 @@ -const Promise = require('bluebird') const models = require('../../models') const requestPromise = require('request-promise') const URLSearchParams = require('url-search-params') @@ -7,7 +6,7 @@ const Decimal = require('decimal.js') const stripe = require('../shared/stripe/stripe')() const Sendmail = require('../mail/mail') const userCustomerCreate = require('../users/userCustomerCreate') -const { notifyNewBounty } = require('../slack') +const { notifyBountyWithErrorHandling } = require('../slack') module.exports = async function orderBuilds(orderParameters) { const { source_id, source_type, currency, provider, amount, email, userId, taskId, plan } = @@ -84,6 +83,8 @@ module.exports = async function orderBuilds(orderParameters) { } }) + // Create invoice item (line item on the invoice) + // Note: We don't store the invoice item ID, only the invoice ID, as the invoice item is part of the invoice const invoiceItem = await stripe.invoiceItems.create({ customer: orderParameters.customer_id, currency: 'usd', @@ -98,6 +99,11 @@ module.exports = async function orderBuilds(orderParameters) { } }) + // Verify invoice item was created successfully + if (!invoiceItem || !invoiceItem.id) { + throw new Error('Failed to create invoice item') + } + const finalizedInvoice = await stripe.invoices.finalizeInvoice(invoice.id) Sendmail.success( { ...orderUser, email: orderParameters.email }, @@ -231,25 +237,17 @@ module.exports = async function orderBuilds(orderParameters) { ) // Send Slack notification for wallet payment (paid immediately) - const shouldNotifySlack = - orderCreated.Task && - orderCreated.User && - !( - orderCreated.Task.dataValues.not_listed === true || - orderCreated.Task.dataValues.private === true - ) - - if (shouldNotifySlack) { - try { - const orderData = { - amount: orderCreated.dataValues.amount, - currency: orderCreated.dataValues.currency || 'USD' - } - await notifyNewBounty(orderCreated.Task.dataValues, orderData, orderCreated.User.dataValues) - } catch (e) { - console.log('error on send slack notification for new bounty', e) - } + // Note: This only runs for wallet payments that complete successfully + const orderData = { + amount: orderCreated.dataValues.amount, + currency: orderCreated.dataValues.currency || 'USD' } + await notifyBountyWithErrorHandling( + orderCreated.Task, + orderData, + orderCreated.User, + 'wallet payment' + ) return orderUpdated } diff --git a/src/modules/orders/orderUpdateAfterStripe.js b/src/modules/orders/orderUpdateAfterStripe.js index e440a8f17..600b26d21 100644 --- a/src/modules/orders/orderUpdateAfterStripe.js +++ b/src/modules/orders/orderUpdateAfterStripe.js @@ -1,7 +1,7 @@ const Promise = require('bluebird') const PaymentMail = require('../mail/payment') const models = require('../../models') -const { notifyNewBounty } = require('../slack') +const { notifyBountyWithErrorHandling } = require('../slack') module.exports = Promise.method( function orderUpdateAfterStripe(order, charge, card, orderParameters, user, task, couponFull) { @@ -22,7 +22,7 @@ module.exports = Promise.method( id: order.dataValues.id } }) - .then(async (updatedOrder) => { + .then(async (_updatedOrder) => { if (orderParameters.plan === 'full') { PaymentMail.support(user, task, order) } @@ -30,22 +30,11 @@ module.exports = Promise.method( // Send Slack notification for new bounty payment if (orderPayload.paid && orderPayload.status === 'succeeded') { - const shouldNotifySlack = - task && - user && - !(task.dataValues.not_listed === true || task.dataValues.private === true) - - if (shouldNotifySlack) { - const orderData = { - amount: order.amount || orderParameters.amount, - currency: order.currency || orderParameters.currency || 'USD' - } - try { - await notifyNewBounty(task.dataValues, orderData, user) - } catch (e) { - console.log('error on send slack notification for new bounty', e) - } + const orderData = { + amount: order.amount || orderParameters.amount, + currency: order.currency || orderParameters.currency || 'USD' } + await notifyBountyWithErrorHandling(task, orderData, user, 'Stripe payment') } if (task.dataValues.assigned) { diff --git a/src/modules/slack/index.js b/src/modules/slack/index.js index 4939e55d2..39fdf0ca7 100644 --- a/src/modules/slack/index.js +++ b/src/modules/slack/index.js @@ -130,7 +130,61 @@ const notifyNewBounty = async (task, order, user) => { }) } +/** + * Helper function to check if a task should trigger Slack notifications + * @param task - The task object (can be Sequelize model instance or plain object) + * @returns boolean - True if task is public and should notify + */ +const shouldNotifyForTask = (task) => { + if (!task) return false + + // Handle both Sequelize model instances and plain objects + const taskData = task.dataValues || task + return !(taskData.not_listed === true || taskData.private === true) +} + +/** + * Helper function to safely send bounty notification with proper error handling + * @param task - The task object + * @param orderData - Order data with amount and currency + * @param user - The user object + * @param context - Optional context string for error logging (e.g., 'wallet payment', 'PayPal payment') + * @returns Promise - True if notification was sent successfully + */ +const notifyBountyWithErrorHandling = async (task, orderData, user, context = 'payment') => { + // Check if task and user exist + if (!task || !user) { + return false + } + + // Check privacy settings + if (!shouldNotifyForTask(task)) { + return false + } + + // Extract data values if Sequelize model instances + const taskData = task.dataValues || task + const userData = user.dataValues || user + + // Prepare order data + const order = { + amount: orderData.amount, + currency: orderData.currency || 'USD' + } + + try { + await notifyNewBounty(taskData, order, userData) + return true + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + console.error(`Error sending Slack notification for new bounty (${context}):`, errorMessage) + return false + } +} + module.exports = { notifyNewIssue, - notifyNewBounty + notifyNewBounty, + shouldNotifyForTask, + notifyBountyWithErrorHandling } diff --git a/src/modules/slack/index.ts b/src/modules/slack/index.ts index ebba615af..9c4c65a06 100644 --- a/src/modules/slack/index.ts +++ b/src/modules/slack/index.ts @@ -3,7 +3,7 @@ * Handles sending notifications to Slack channel for new issues and bounties */ -import * as requestPromise from 'request-promise' +import requestPromise from 'request-promise' import secrets from '../../config/secrets' import type { Task, User, OrderData, SlackMessagePayload } from './types' @@ -164,3 +164,62 @@ export const notifyNewBounty = async ( ] }) } + +/** + * Helper function to check if a task should trigger Slack notifications + * @param task - The task object (can be Sequelize model instance or plain object) + * @returns boolean - True if task is public and should notify + */ +export const shouldNotifyForTask = (task: Task | { dataValues?: Task } | null | undefined): boolean => { + if (!task) { + return false + } + + // Handle both Sequelize model instances and plain objects + const taskData = (task as { dataValues?: Task }).dataValues || (task as Task) + return !(taskData.not_listed === true || taskData.private === true) +} + +/** + * Helper function to safely send bounty notification with proper error handling + * @param task - The task object + * @param orderData - Order data with amount and currency + * @param user - The user object + * @param context - Optional context string for error logging (e.g., 'wallet payment', 'PayPal payment') + * @returns Promise - True if notification was sent successfully + */ +export const notifyBountyWithErrorHandling = async ( + task: Task | { dataValues?: Task } | null | undefined, + orderData: OrderData | null | undefined, + user: User | { dataValues?: User } | null | undefined, + context: string = 'payment' +): Promise => { + // Check if task and user exist + if (!task || !user) { + return false + } + + // Check privacy settings + if (!shouldNotifyForTask(task)) { + return false + } + + // Extract data values if Sequelize model instances + const taskData = (task as { dataValues?: Task }).dataValues || (task as Task) + const userData = (user as { dataValues?: User }).dataValues || (user as User) + + // Prepare order data + const order: OrderData = { + amount: orderData?.amount || 0, + currency: orderData?.currency || 'USD' + } + + try { + await notifyNewBounty(taskData, order, userData) + return true + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + console.error(`Error sending Slack notification for new bounty (${context}):`, errorMessage) + return false + } +} diff --git a/src/modules/webhooks/chargeSucceeded.js b/src/modules/webhooks/chargeSucceeded.js index 2461a6b1c..e2af24c87 100644 --- a/src/modules/webhooks/chargeSucceeded.js +++ b/src/modules/webhooks/chargeSucceeded.js @@ -1,7 +1,7 @@ const models = require('../../models') const i18n = require('i18n') const SendMail = require('../mail/mail') -const { notifyNewBounty } = require('../slack') +const { notifyBountyWithErrorHandling } = require('../slack') const sendEmailSuccess = (event, paid, status, order, req, res) => { return models.User.findOne({ @@ -57,29 +57,16 @@ const updateOrder = async (event, paid, status, req, res) => { }) if (orderUpdated) { - const shouldNotifySlack = - orderUpdated.Task && - orderUpdated.User && - !( - orderUpdated.Task.dataValues.not_listed === true || - orderUpdated.Task.dataValues.private === true - ) - - if (shouldNotifySlack) { - const orderData = { - amount: orderUpdated.amount, - currency: orderUpdated.currency || 'USD' - } - try { - await notifyNewBounty( - orderUpdated.Task.dataValues, - orderData, - orderUpdated.User.dataValues - ) - } catch (e) { - console.log('error on send slack notification for new bounty', e) - } + const orderData = { + amount: orderUpdated.amount, + currency: orderUpdated.currency || 'USD' } + await notifyBountyWithErrorHandling( + orderUpdated.Task, + orderData, + orderUpdated.User, + 'Stripe charge succeeded' + ) } } diff --git a/src/modules/webhooks/chargeUpdated.js b/src/modules/webhooks/chargeUpdated.js index c7fb5a4e2..89f4cf50d 100644 --- a/src/modules/webhooks/chargeUpdated.js +++ b/src/modules/webhooks/chargeUpdated.js @@ -2,7 +2,7 @@ const models = require('../../models') const i18n = require('i18n') const moment = require('moment') const SendMail = require('../mail/mail') -const { notifyNewBounty } = require('../slack') +const { notifyBountyWithErrorHandling } = require('../slack') module.exports = async function chargeUpdated(event, paid, status, req, res) { if (event?.data?.object?.source?.id) { @@ -48,29 +48,16 @@ module.exports = async function chargeUpdated(event, paid, status, req, res) { }) if (orderUpdated) { - const shouldNotifySlack = - orderUpdated.Task && - orderUpdated.User && - !( - orderUpdated.Task.dataValues.not_listed === true || - orderUpdated.Task.dataValues.private === true - ) - - if (shouldNotifySlack) { - const orderData = { - amount: orderUpdated.amount, - currency: orderUpdated.currency || 'USD' - } - try { - await notifyNewBounty( - orderUpdated.Task.dataValues, - orderData, - orderUpdated.User.dataValues - ) - } catch (e) { - console.log('error on send slack notification for new bounty', e) - } + const orderData = { + amount: orderUpdated.amount, + currency: orderUpdated.currency || 'USD' } + await notifyBountyWithErrorHandling( + orderUpdated.Task, + orderData, + orderUpdated.User, + 'Stripe charge updated' + ) } } } diff --git a/src/modules/webhooks/invoicePaymentSucceeded.js b/src/modules/webhooks/invoicePaymentSucceeded.js index 2a1dd5ea1..066a51d97 100644 --- a/src/modules/webhooks/invoicePaymentSucceeded.js +++ b/src/modules/webhooks/invoicePaymentSucceeded.js @@ -5,7 +5,7 @@ const SendMail = require('../mail/mail') const WalletMail = require('../mail/wallet') const stripe = require('../shared/stripe/stripe')() const { FAILED_REASON, CURRENCIES, formatStripeAmount } = require('./constants') -const { notifyNewBounty } = require('../slack') +const { notifyBountyWithErrorHandling } = require('../slack') module.exports = async function invoicePaymentSucceeded(event, req, res) { return models.User.findOne({ @@ -47,26 +47,16 @@ module.exports = async function invoicePaymentSucceeded(event, req, res) { }) if (orderUpdated && orderUpdated.Task && orderUpdated.User) { - const shouldNotifySlack = !( - orderUpdated.Task.dataValues.not_listed === true || - orderUpdated.Task.dataValues.private === true - ) - - if (shouldNotifySlack) { - const orderData = { - amount: orderUpdated.amount, - currency: orderUpdated.currency || 'USD' - } - try { - await notifyNewBounty( - orderUpdated.Task.dataValues, - orderData, - orderUpdated.User.dataValues - ) - } catch (e) { - console.log('error on send slack notification for new bounty', e) - } + const orderData = { + amount: orderUpdated.amount, + currency: orderUpdated.currency || 'USD' } + await notifyBountyWithErrorHandling( + orderUpdated.Task, + orderData, + orderUpdated.User, + 'Stripe invoice payment succeeded' + ) } } return res.status(200).json(event) diff --git a/src/modules/webhooks/invoiceUpdated.js b/src/modules/webhooks/invoiceUpdated.js index a2748ade7..aff2f2e66 100644 --- a/src/modules/webhooks/invoiceUpdated.js +++ b/src/modules/webhooks/invoiceUpdated.js @@ -5,7 +5,7 @@ const SendMail = require('../mail/mail') const WalletMail = require('../mail/wallet') const stripe = require('../shared/stripe/stripe')() const { FAILED_REASON, CURRENCIES, formatStripeAmount } = require('./constants') -const { notifyNewBounty } = require('../slack') +const { notifyBountyWithErrorHandling } = require('../slack') module.exports = async function invoiceUpdated(event, req, res) { // eslint-disable-next-line no-case-declarations @@ -84,29 +84,16 @@ module.exports = async function invoiceUpdated(event, req, res) { ) // Send Slack notification for invoice payment completion - const shouldNotifySlack = - orderUpdated.Task && - orderUpdated.User && - !( - orderUpdated.Task.dataValues.not_listed === true || - orderUpdated.Task.dataValues.private === true - ) - - if (shouldNotifySlack) { - const orderData = { - amount: orderUpdated.amount, - currency: orderUpdated.currency || 'USD' - } - try { - await notifyNewBounty( - orderUpdated.Task.dataValues, - orderData, - orderUpdated.User.dataValues - ) - } catch (e) { - console.log('error on send slack notification for new bounty', e) - } + const orderData = { + amount: orderUpdated.amount, + currency: orderUpdated.currency || 'USD' } + await notifyBountyWithErrorHandling( + orderUpdated.Task, + orderData, + orderUpdated.User, + 'Stripe invoice payment' + ) } } return res.status(200).json(event) diff --git a/test/order.test.js b/test/order.test.js index 24458f4be..998edd582 100644 --- a/test/order.test.js +++ b/test/order.test.js @@ -952,11 +952,20 @@ describe('Orders', () => { paid: false }) - await orderAuthorize({ + const result = await orderAuthorize({ token: 'TEST_TOKEN', PayerID: 'TEST_PAYER_ID' }) + // Verify the order was marked as paid + const updatedOrder = await models.Order.findOne({ + where: { token: 'TEST_TOKEN' } + }) + expect(updatedOrder.paid).to.be.true + + // Wait a bit for the async notification to complete (it uses .catch() which is fire-and-forget) + await new Promise((resolve) => setTimeout(resolve, 200)) + // Notification should be called when PayPal payment completes expect(slackSpy).to.have.been.called() } finally { From 1bc2ac7d98e02a0ba8b6eee77265877b0d9f6c78 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Sat, 17 Jan 2026 05:55:28 +0800 Subject: [PATCH 14/20] fix lint error --- .../issue-actions-by-role.tsx | 26 +++++++++---------- .../issue-payment-drawer.tsx | 2 -- frontend/src/containers/task.js | 3 ++- src/modules/slack/index.ts | 4 ++- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/design-library/atoms/buttons/issue-actions-by-role/issue-actions-by-role.tsx b/frontend/src/components/design-library/atoms/buttons/issue-actions-by-role/issue-actions-by-role.tsx index f1aafd4b4..3e6e7a951 100644 --- a/frontend/src/components/design-library/atoms/buttons/issue-actions-by-role/issue-actions-by-role.tsx +++ b/frontend/src/components/design-library/atoms/buttons/issue-actions-by-role/issue-actions-by-role.tsx @@ -53,19 +53,19 @@ const IssueActionsByRole = ({ fetchPullRequestData, fetchCustomer, customer, - addNotification, - updateTask, - task, - createOrder, - order, - fetchWallet, - wallet, - listWallets, - wallets, - fetchTask, - syncTask, - validateCoupon, - couponStoreState + addNotification, + updateTask, + task, + createOrder, + order, + fetchWallet, + wallet, + listWallets, + wallets, + fetchTask, + syncTask, + validateCoupon, + couponStoreState }: IssueActionsProps) => { const { data } = issue diff --git a/frontend/src/components/design-library/molecules/drawers/issue-payment-drawer/issue-payment-drawer.tsx b/frontend/src/components/design-library/molecules/drawers/issue-payment-drawer/issue-payment-drawer.tsx index 298de9e63..a8c5cb017 100644 --- a/frontend/src/components/design-library/molecules/drawers/issue-payment-drawer/issue-payment-drawer.tsx +++ b/frontend/src/components/design-library/molecules/drawers/issue-payment-drawer/issue-payment-drawer.tsx @@ -1,7 +1,5 @@ import React, { useEffect, useMemo, useState } from 'react' import { FormattedMessage, defineMessages, useIntl } from 'react-intl' -import { connect } from 'react-redux' -import { validateCoupon } from '../../../../../actions/couponActions' import PaymentDrawer from 'design-library/molecules/drawers/payment-drawer/payment-drawer' diff --git a/frontend/src/containers/task.js b/frontend/src/containers/task.js index d02664c06..d24bea500 100644 --- a/frontend/src/containers/task.js +++ b/frontend/src/containers/task.js @@ -127,7 +127,8 @@ const mapDispatchToProps = (dispatch, ownProps) => { dispatch(fetchPullRequestData(owner, repositoryName, pullRequestId, taskId)), cleanPullRequestDataState: () => dispatch(cleanPullRequestDataState()), fetchAccount: () => dispatch(fetchAccount()), - validateCoupon: (code, originalOrderPrice) => dispatch(validateCoupon(code, originalOrderPrice)), + validateCoupon: (code, originalOrderPrice) => + dispatch(validateCoupon(code, originalOrderPrice)), // For account menu and bottom bar props signOut: () => dispatch(logOut()), getInfo: () => dispatch(getInfoAction()) diff --git a/src/modules/slack/index.ts b/src/modules/slack/index.ts index 9c4c65a06..e762c31a3 100644 --- a/src/modules/slack/index.ts +++ b/src/modules/slack/index.ts @@ -170,7 +170,9 @@ export const notifyNewBounty = async ( * @param task - The task object (can be Sequelize model instance or plain object) * @returns boolean - True if task is public and should notify */ -export const shouldNotifyForTask = (task: Task | { dataValues?: Task } | null | undefined): boolean => { +export const shouldNotifyForTask = ( + task: Task | { dataValues?: Task } | null | undefined +): boolean => { if (!task) { return false } From f827481961cf2b1e1d0f2c87df9dee15a8e7c591 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Tue, 20 Jan 2026 12:11:53 +0800 Subject: [PATCH 15/20] Fix slack notification and stabilize tests --- src/models/planSchema.js | 4 + src/modules/orders/orderAuthorize.js | 19 ++-- src/modules/orders/orderBuilds.js | 43 +++++--- src/modules/orders/orderUpdateAfterStripe.js | 4 +- src/modules/tasks/taskBuilds.js | 4 +- src/modules/users/userBuilds.js | 2 +- src/modules/webhooks/chargeSucceeded.js | 4 +- src/modules/webhooks/chargeUpdated.js | 4 +- .../webhooks/invoicePaymentSucceeded.js | 4 +- src/modules/webhooks/invoiceUpdated.js | 4 +- test/helpers/index.js | 7 +- test/order.test.js | 102 ++++++++++++------ test/user.test.js | 21 +++- 13 files changed, 146 insertions(+), 76 deletions(-) diff --git a/src/models/planSchema.js b/src/models/planSchema.js index 8071f6fd0..4fa72668f 100644 --- a/src/models/planSchema.js +++ b/src/models/planSchema.js @@ -1,5 +1,9 @@ module.exports = (sequelize, DataTypes) => { const PlanSchema = sequelize.define('PlanSchema', { + plan: { + type: DataTypes.STRING, + allowNull: false + }, name: DataTypes.STRING, description: DataTypes.STRING, fee: DataTypes.DECIMAL, diff --git a/src/modules/orders/orderAuthorize.js b/src/modules/orders/orderAuthorize.js index c833cf955..91400d73c 100644 --- a/src/modules/orders/orderAuthorize.js +++ b/src/modules/orders/orderAuthorize.js @@ -3,7 +3,7 @@ const Promise = require('bluebird') const requestPromise = require('request-promise') const models = require('../../models') const comment = require('../bot/comment') -const { notifyBountyWithErrorHandling } = require('../slack') +const slack = require('../slack') module.exports = Promise.method(function orderAuthorize(orderParameters) { return requestPromise({ @@ -60,7 +60,7 @@ module.exports = Promise.method(function orderAuthorize(orderParameters) { return Promise.all([ models.User.findByPk(orderData.userId), models.Task.findByPk(orderData.TaskId) - ]).spread(async (user, task) => { + ]).then(async ([user, task]) => { if (orderData.paid) { comment(orderData, task) PaymentMail.success(user, task, orderData.amount) @@ -70,12 +70,15 @@ module.exports = Promise.method(function orderAuthorize(orderParameters) { amount: orderData.amount, currency: orderData.currency || 'USD' } - await notifyBountyWithErrorHandling( - task, - orderDataForNotification, - user, - 'PayPal payment' - ) + // Avoid even invoking notification helper for private/not_listed tasks + if (slack.shouldNotifyForTask(task)) { + await slack.notifyBountyWithErrorHandling( + task, + orderDataForNotification, + user, + 'PayPal payment' + ) + } } else { PaymentMail.error(user.dataValues, task, orderData.amount) } diff --git a/src/modules/orders/orderBuilds.js b/src/modules/orders/orderBuilds.js index 7d25576ae..f0d0f3a06 100644 --- a/src/modules/orders/orderBuilds.js +++ b/src/modules/orders/orderBuilds.js @@ -6,7 +6,7 @@ const Decimal = require('decimal.js') const stripe = require('../shared/stripe/stripe')() const Sendmail = require('../mail/mail') const userCustomerCreate = require('../users/userCustomerCreate') -const { notifyBountyWithErrorHandling } = require('../slack') +const slack = require('../slack') module.exports = async function orderBuilds(orderParameters) { const { source_id, source_type, currency, provider, amount, email, userId, taskId, plan } = @@ -211,10 +211,15 @@ module.exports = async function orderBuilds(orderParameters) { } }) - const currentBalance = wallet.balance - const enoughBalance = new Decimal(currentBalance).greaterThanOrEqualTo( - new Decimal(orderParameters.amount) - ) + if (!wallet) { + throw new Error(`Wallet with id ${orderParameters.walletId} not found`) + } + + // Wallet balance is calculated from WalletOrders via afterFind hook + // The balance field is updated by the hook after findOne + // Convert to Decimal for comparison (balance is a string after hook processing) + const currentBalance = new Decimal(wallet.balance || '0.00') + const enoughBalance = currentBalance.greaterThanOrEqualTo(new Decimal(orderParameters.amount)) if (!enoughBalance) { throw new Error( @@ -238,16 +243,26 @@ module.exports = async function orderBuilds(orderParameters) { // Send Slack notification for wallet payment (paid immediately) // Note: This only runs for wallet payments that complete successfully - const orderData = { - amount: orderCreated.dataValues.amount, - currency: orderCreated.dataValues.currency || 'USD' + // Reload order with associations to ensure Task and User are available + const orderWithAssociations = await models.Order.findByPk(orderCreated.dataValues.id, { + include: [models.Task, models.User] + }) + + if (orderWithAssociations && orderWithAssociations.Task && orderWithAssociations.User) { + const orderData = { + amount: orderCreated.dataValues.amount, + currency: orderCreated.dataValues.currency || 'USD' + } + // Avoid even invoking notification helper for private/not_listed tasks + if (slack.shouldNotifyForTask(orderWithAssociations.Task)) { + await slack.notifyBountyWithErrorHandling( + orderWithAssociations.Task, + orderData, + orderWithAssociations.User, + 'wallet payment' + ) + } } - await notifyBountyWithErrorHandling( - orderCreated.Task, - orderData, - orderCreated.User, - 'wallet payment' - ) return orderUpdated } diff --git a/src/modules/orders/orderUpdateAfterStripe.js b/src/modules/orders/orderUpdateAfterStripe.js index 600b26d21..1edbc7082 100644 --- a/src/modules/orders/orderUpdateAfterStripe.js +++ b/src/modules/orders/orderUpdateAfterStripe.js @@ -1,7 +1,7 @@ const Promise = require('bluebird') const PaymentMail = require('../mail/payment') const models = require('../../models') -const { notifyBountyWithErrorHandling } = require('../slack') +const slack = require('../slack') module.exports = Promise.method( function orderUpdateAfterStripe(order, charge, card, orderParameters, user, task, couponFull) { @@ -34,7 +34,7 @@ module.exports = Promise.method( amount: order.amount || orderParameters.amount, currency: order.currency || orderParameters.currency || 'USD' } - await notifyBountyWithErrorHandling(task, orderData, user, 'Stripe payment') + await slack.notifyBountyWithErrorHandling(task, orderData, user, 'Stripe payment') } if (task.dataValues.assigned) { diff --git a/src/modules/tasks/taskBuilds.js b/src/modules/tasks/taskBuilds.js index b6670cc7f..fed4a3e68 100644 --- a/src/modules/tasks/taskBuilds.js +++ b/src/modules/tasks/taskBuilds.js @@ -10,7 +10,7 @@ const userExists = require('../users').userExists // const userOrganizations = require('../users/userOrganizations') const project = require('../projectHelpers') const issueAddedComment = require('../bot/issueAddedComment') -const { notifyNewIssue } = require('../slack') +const slack = require('../slack') module.exports = Promise.method(async function taskBuilds(taskParameters) { const repoUrl = taskParameters.url @@ -108,7 +108,7 @@ module.exports = Promise.method(async function taskBuilds(taskParameters) { const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) if (isTaskPublic) { issueAddedComment(task) - notifyNewIssue(taskData, userData) + slack.notifyNewIssue(taskData, userData) } return { ...taskData, ProjectId: taskData.ProjectId } diff --git a/src/modules/users/userBuilds.js b/src/modules/users/userBuilds.js index 1aaab6695..93fc0bc2d 100644 --- a/src/modules/users/userBuilds.js +++ b/src/modules/users/userBuilds.js @@ -30,7 +30,7 @@ module.exports = Promise.method(async function userBuilds(userParameters) { if (selectedTypeIds && selectedTypeIds.length > 0) { await user.setTypes(selectedTypeIds) const userWithTypes = await models.User.findByPk(id, { - include: { model: models.Type } + include: { model: models.Type, as: 'Types' } }) return userWithTypes } diff --git a/src/modules/webhooks/chargeSucceeded.js b/src/modules/webhooks/chargeSucceeded.js index e2af24c87..e6ab3ad91 100644 --- a/src/modules/webhooks/chargeSucceeded.js +++ b/src/modules/webhooks/chargeSucceeded.js @@ -1,7 +1,7 @@ const models = require('../../models') const i18n = require('i18n') const SendMail = require('../mail/mail') -const { notifyBountyWithErrorHandling } = require('../slack') +const slack = require('../slack') const sendEmailSuccess = (event, paid, status, order, req, res) => { return models.User.findOne({ @@ -61,7 +61,7 @@ const updateOrder = async (event, paid, status, req, res) => { amount: orderUpdated.amount, currency: orderUpdated.currency || 'USD' } - await notifyBountyWithErrorHandling( + await slack.notifyBountyWithErrorHandling( orderUpdated.Task, orderData, orderUpdated.User, diff --git a/src/modules/webhooks/chargeUpdated.js b/src/modules/webhooks/chargeUpdated.js index 89f4cf50d..f7bdd4eea 100644 --- a/src/modules/webhooks/chargeUpdated.js +++ b/src/modules/webhooks/chargeUpdated.js @@ -2,7 +2,7 @@ const models = require('../../models') const i18n = require('i18n') const moment = require('moment') const SendMail = require('../mail/mail') -const { notifyBountyWithErrorHandling } = require('../slack') +const slack = require('../slack') module.exports = async function chargeUpdated(event, paid, status, req, res) { if (event?.data?.object?.source?.id) { @@ -52,7 +52,7 @@ module.exports = async function chargeUpdated(event, paid, status, req, res) { amount: orderUpdated.amount, currency: orderUpdated.currency || 'USD' } - await notifyBountyWithErrorHandling( + await slack.notifyBountyWithErrorHandling( orderUpdated.Task, orderData, orderUpdated.User, diff --git a/src/modules/webhooks/invoicePaymentSucceeded.js b/src/modules/webhooks/invoicePaymentSucceeded.js index 066a51d97..faba8fa89 100644 --- a/src/modules/webhooks/invoicePaymentSucceeded.js +++ b/src/modules/webhooks/invoicePaymentSucceeded.js @@ -5,7 +5,7 @@ const SendMail = require('../mail/mail') const WalletMail = require('../mail/wallet') const stripe = require('../shared/stripe/stripe')() const { FAILED_REASON, CURRENCIES, formatStripeAmount } = require('./constants') -const { notifyBountyWithErrorHandling } = require('../slack') +const slack = require('../slack') module.exports = async function invoicePaymentSucceeded(event, req, res) { return models.User.findOne({ @@ -51,7 +51,7 @@ module.exports = async function invoicePaymentSucceeded(event, req, res) { amount: orderUpdated.amount, currency: orderUpdated.currency || 'USD' } - await notifyBountyWithErrorHandling( + await slack.notifyBountyWithErrorHandling( orderUpdated.Task, orderData, orderUpdated.User, diff --git a/src/modules/webhooks/invoiceUpdated.js b/src/modules/webhooks/invoiceUpdated.js index aff2f2e66..889658744 100644 --- a/src/modules/webhooks/invoiceUpdated.js +++ b/src/modules/webhooks/invoiceUpdated.js @@ -5,7 +5,7 @@ const SendMail = require('../mail/mail') const WalletMail = require('../mail/wallet') const stripe = require('../shared/stripe/stripe')() const { FAILED_REASON, CURRENCIES, formatStripeAmount } = require('./constants') -const { notifyBountyWithErrorHandling } = require('../slack') +const slack = require('../slack') module.exports = async function invoiceUpdated(event, req, res) { // eslint-disable-next-line no-case-declarations @@ -88,7 +88,7 @@ module.exports = async function invoiceUpdated(event, req, res) { amount: orderUpdated.amount, currency: orderUpdated.currency || 'USD' } - await notifyBountyWithErrorHandling( + await slack.notifyBountyWithErrorHandling( orderUpdated.Task, orderData, orderUpdated.User, diff --git a/test/helpers/index.js b/test/helpers/index.js index 9889c157f..53cfd7f56 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -1,10 +1,11 @@ const models = require('../../src/models') -const testEmail = `teste+${Math.random() * 100}@gmail.com` const testPassword = 'test12345678' const testName = 'Test' +const generateTestEmail = () => `teste+${Math.random()}@gmail.com` + const register = (agent, params = {}) => { - params.email = params.email || testEmail + params.email = params.email || generateTestEmail() params.password = params.password || testPassword params.confirmPassword = params.password || testPassword params.name = params.name || testName @@ -17,7 +18,7 @@ const register = (agent, params = {}) => { } const login = (agent, params = {}) => { - params.username = params.email || testEmail + params.username = params.email params.password = params.password || testPassword return agent.post('/authorize/local').send(params).type('form') } diff --git a/test/order.test.js b/test/order.test.js index 998edd582..917a01ff0 100644 --- a/test/order.test.js +++ b/test/order.test.js @@ -23,6 +23,29 @@ describe('Orders', () => { await truncateModels(models.Order) await truncateModels(models.Transfer) await truncateModels(models.Wallet) + + // PlanSchemas are required by order creation for "open source" plan. + // They are not truncated above, so ensure they exist deterministically. + await models.PlanSchema.findOrCreate({ + where: { plan: 'open source', name: 'Open Source - default', feeType: 'charge' }, + defaults: { + plan: 'open source', + name: 'Open Source - default', + description: 'open source', + fee: 8, + feeType: 'charge' + } + }) + await models.PlanSchema.findOrCreate({ + where: { plan: 'open source', name: 'Open Source - no fee', feeType: 'charge' }, + defaults: { + plan: 'open source', + name: 'Open Source - no fee', + description: 'open source with no fee', + fee: 0, + feeType: 'charge' + } + }) }) afterEach(async () => { nock.cleanAll() @@ -160,16 +183,28 @@ describe('Orders', () => { it('should call notifyNewBounty when wallet payment completes for a public task', async () => { chai.use(spies) const slackModule = require('../src/modules/slack') - const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + // Spy on notifyBountyWithErrorHandling since that's what's actually called + const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') const orderBuilds = require('../src/modules/orders').orderBuilds try { const user = await registerAndLogin(agent) const wallet = await models.Wallet.create({ name: 'Test Wallet', - balance: 500, + balance: 0, userId: user.body.id }) + // Create a WalletOrder to give the wallet balance (wallet balance is calculated from WalletOrders) + // The wallet balance is calculated as: sum of paid WalletOrders - sum of succeeded wallet Orders + await models.WalletOrder.create({ + walletId: wallet.id, + amount: 500, + currency: 'USD', + status: 'paid', + paid: true + }) + // Reload wallet to trigger afterFind hook and recalculate balance + await wallet.reload() const task = await models.Task.create({ url: 'https://github.com/test/repo/issues/4', userId: user.body.id, @@ -190,29 +225,41 @@ describe('Orders', () => { // Notification should be called when wallet payment completes expect(slackSpy).to.have.been.called() - expect(slackSpy).to.have.been.called.with( - task.dataValues, - { amount: 200, currency: 'USD' }, - user.body - ) + // Verify it was called with correct order data and context + const call = slackSpy.__spy.calls[0] + expect(Number(call[1].amount)).to.equal(200) + expect(call[1].currency).to.equal('USD') + expect(call[3]).to.equal('wallet payment') } finally { - chai.spy.restore(slackModule, 'notifyNewBounty') + chai.spy.restore(slackModule, 'notifyBountyWithErrorHandling') } }) it('should not call notifyNewBounty when wallet payment completes for a private task', async () => { chai.use(spies) const slackModule = require('../src/modules/slack') - const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + // Spy on notifyBountyWithErrorHandling since that's what's actually called + const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') const orderBuilds = require('../src/modules/orders').orderBuilds try { const user = await registerAndLogin(agent) const wallet = await models.Wallet.create({ name: 'Test Wallet', - balance: 500, + balance: 0, userId: user.body.id }) + // Create a WalletOrder to give the wallet balance (wallet balance is calculated from WalletOrders) + // The wallet balance is calculated as: sum of paid WalletOrders - sum of succeeded wallet Orders + await models.WalletOrder.create({ + walletId: wallet.id, + amount: 500, + currency: 'USD', + status: 'paid', + paid: true + }) + // Reload wallet to trigger afterFind hook and recalculate balance + await wallet.reload() const task = await models.Task.create({ url: 'https://github.com/test/repo/issues/5', userId: user.body.id, @@ -233,28 +280,13 @@ describe('Orders', () => { // Notification should NOT be called for private tasks expect(slackSpy).to.not.have.been.called() } finally { - chai.spy.restore(slackModule, 'notifyNewBounty') + chai.spy.restore(slackModule, 'notifyBountyWithErrorHandling') } }) describe('Order with Plan', () => { let PlanSchema - beforeEach(async () => { - PlanSchema = await models.PlanSchema.build({ - plan: 'open source', - name: 'Open Source - default', - description: 'open source', - fee: 8, - feeType: 'charge' - }) - PlanSchema = await models.PlanSchema.build({ - plan: 'open source', - name: 'Open Source - no fee', - description: 'open source with no fee', - fee: 0, - feeType: 'charge' - }) - }) + beforeEach(async () => {}) it('should create a new order with a plan', async () => { const user = await registerAndLogin(agent) const res = await agent @@ -909,7 +941,8 @@ describe('Orders', () => { it('should call notifyNewBounty when PayPal payment completes for a public task', async () => { chai.use(spies) const slackModule = require('../src/modules/slack') - const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + // Spy on notifyBountyWithErrorHandling since that's what's actually called + const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') const orderAuthorize = require('../src/modules/orders').orderAuthorize // Mock PayPal API responses @@ -963,13 +996,15 @@ describe('Orders', () => { }) expect(updatedOrder.paid).to.be.true - // Wait a bit for the async notification to complete (it uses .catch() which is fire-and-forget) - await new Promise((resolve) => setTimeout(resolve, 200)) - // Notification should be called when PayPal payment completes expect(slackSpy).to.have.been.called() + // Verify it was called with correct order data and context + const call = slackSpy.__spy.calls[0] + expect(Number(call[1].amount)).to.equal(200) + expect(call[1].currency).to.equal('USD') + expect(call[3]).to.equal('PayPal payment') } finally { - chai.spy.restore(slackModule, 'notifyNewBounty') + chai.spy.restore(slackModule, 'notifyBountyWithErrorHandling') nock.cleanAll() } }) @@ -977,7 +1012,8 @@ describe('Orders', () => { it('should not call notifyNewBounty when PayPal payment completes for a private task', async () => { chai.use(spies) const slackModule = require('../src/modules/slack') - const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + // Spy on notifyBountyWithErrorHandling since that's what's actually called + const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') const orderAuthorize = require('../src/modules/orders').orderAuthorize // Mock PayPal API responses diff --git a/test/user.test.js b/test/user.test.js index 57a7d8bd5..fe5ae5916 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -171,18 +171,29 @@ describe('Users', () => { }) }) it('register with user Types', async () => { + // Ensure the types exist, then use their real IDs (avoid relying on seed order/IDs) + const { Type } = require('../src/models') + const [funding] = await Type.findOrCreate({ where: { name: 'funding' }, defaults: { name: 'funding' } }) + const [contributor] = await Type.findOrCreate({ + where: { name: 'contributor' }, + defaults: { name: 'contributor' } + }) + const res = await agent .post('/auth/register') - .send({ email: 'teste4343434322222@gmail.com', password: 'test', Types: ['1', '2'] }) + .send({ + email: 'teste4343434322222@gmail.com', + password: 'test', + Types: [String(funding.id), String(contributor.id)] + }) .expect('Content-Type', /json/) .expect(200) expect(res.statusCode).to.equal(200) expect(res.body).to.exist expect(res.body.Types).to.exist - expect(res.body.Types[0].id).to.equal(1) - expect(res.body.Types[0].name).to.equal('funding') - expect(res.body.Types[1].id).to.equal(2) - expect(res.body.Types[1].name).to.equal('contributor') + const typeNames = (res.body.Types || []).map((t) => t.name) + expect(typeNames).to.include('funding') + expect(typeNames).to.include('contributor') }) }) From 3af471b1e3797a15aff67883f937687215914c61 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Tue, 27 Jan 2026 17:41:39 +0800 Subject: [PATCH 16/20] Refactor slack module and move to shared directory --- src/modules/orders/orderAuthorize.js | 17 +++----- src/modules/orders/orderBuilds.js | 17 +++----- src/modules/orders/orderUpdateAfterStripe.js | 4 +- src/modules/{ => shared}/slack/index.js | 27 ++++-------- src/modules/{ => shared}/slack/index.ts | 43 ++++--------------- src/modules/{ => shared}/slack/types.ts | 0 src/modules/tasks/taskBuilds.js | 10 ++--- src/modules/webhooks/chargeSucceeded.js | 4 +- src/modules/webhooks/chargeUpdated.js | 4 +- .../webhooks/invoicePaymentSucceeded.js | 4 +- src/modules/webhooks/invoiceUpdated.js | 4 +- test/order.test.js | 16 +++---- test/slack.test.js | 4 +- 13 files changed, 53 insertions(+), 101 deletions(-) rename src/modules/{ => shared}/slack/index.js (80%) rename src/modules/{ => shared}/slack/index.ts (75%) rename src/modules/{ => shared}/slack/types.ts (100%) diff --git a/src/modules/orders/orderAuthorize.js b/src/modules/orders/orderAuthorize.js index 91400d73c..51fe68489 100644 --- a/src/modules/orders/orderAuthorize.js +++ b/src/modules/orders/orderAuthorize.js @@ -3,7 +3,7 @@ const Promise = require('bluebird') const requestPromise = require('request-promise') const models = require('../../models') const comment = require('../bot/comment') -const slack = require('../slack') +const slack = require('../shared/slack') module.exports = Promise.method(function orderAuthorize(orderParameters) { return requestPromise({ @@ -70,15 +70,12 @@ module.exports = Promise.method(function orderAuthorize(orderParameters) { amount: orderData.amount, currency: orderData.currency || 'USD' } - // Avoid even invoking notification helper for private/not_listed tasks - if (slack.shouldNotifyForTask(task)) { - await slack.notifyBountyWithErrorHandling( - task, - orderDataForNotification, - user, - 'PayPal payment' - ) - } + await slack.notifyBounty( + task, + orderDataForNotification, + user, + 'PayPal payment' + ) } else { PaymentMail.error(user.dataValues, task, orderData.amount) } diff --git a/src/modules/orders/orderBuilds.js b/src/modules/orders/orderBuilds.js index f0d0f3a06..5b71dd061 100644 --- a/src/modules/orders/orderBuilds.js +++ b/src/modules/orders/orderBuilds.js @@ -6,7 +6,7 @@ const Decimal = require('decimal.js') const stripe = require('../shared/stripe/stripe')() const Sendmail = require('../mail/mail') const userCustomerCreate = require('../users/userCustomerCreate') -const slack = require('../slack') +const slack = require('../shared/slack') module.exports = async function orderBuilds(orderParameters) { const { source_id, source_type, currency, provider, amount, email, userId, taskId, plan } = @@ -253,15 +253,12 @@ module.exports = async function orderBuilds(orderParameters) { amount: orderCreated.dataValues.amount, currency: orderCreated.dataValues.currency || 'USD' } - // Avoid even invoking notification helper for private/not_listed tasks - if (slack.shouldNotifyForTask(orderWithAssociations.Task)) { - await slack.notifyBountyWithErrorHandling( - orderWithAssociations.Task, - orderData, - orderWithAssociations.User, - 'wallet payment' - ) - } + await slack.notifyBounty( + orderWithAssociations.Task, + orderData, + orderWithAssociations.User, + 'wallet payment' + ) } return orderUpdated diff --git a/src/modules/orders/orderUpdateAfterStripe.js b/src/modules/orders/orderUpdateAfterStripe.js index 1edbc7082..5b194e246 100644 --- a/src/modules/orders/orderUpdateAfterStripe.js +++ b/src/modules/orders/orderUpdateAfterStripe.js @@ -1,7 +1,7 @@ const Promise = require('bluebird') const PaymentMail = require('../mail/payment') const models = require('../../models') -const slack = require('../slack') +const slack = require('../shared/slack') module.exports = Promise.method( function orderUpdateAfterStripe(order, charge, card, orderParameters, user, task, couponFull) { @@ -34,7 +34,7 @@ module.exports = Promise.method( amount: order.amount || orderParameters.amount, currency: order.currency || orderParameters.currency || 'USD' } - await slack.notifyBountyWithErrorHandling(task, orderData, user, 'Stripe payment') + await slack.notifyBounty(task, orderData, user, 'Stripe payment') } if (task.dataValues.assigned) { diff --git a/src/modules/slack/index.js b/src/modules/shared/slack/index.js similarity index 80% rename from src/modules/slack/index.js rename to src/modules/shared/slack/index.js index 39fdf0ca7..9570fa934 100644 --- a/src/modules/slack/index.js +++ b/src/modules/shared/slack/index.js @@ -1,5 +1,5 @@ const requestPromise = require('request-promise') -const secrets = require('../../config/secrets') +const secrets = require('../../../config/secrets') const sendSlackMessage = async (payload) => { const webhookUrl = process.env.SLACK_WEBHOOK_URL || secrets.slack?.webhookUrl @@ -34,6 +34,8 @@ const formatCurrency = (amount, currency = 'USD') => { const notifyNewIssue = async (task, user) => { if (!task?.id) return false + if (!shouldNotifyForTask(task)) return false + const username = user?.username || user?.name || 'Unknown' return await sendSlackMessage({ @@ -80,7 +82,7 @@ const notifyNewIssue = async (task, user) => { }) } -const notifyNewBounty = async (task, order, user) => { +const notifyBountyOnSlack = async (task, order, user) => { if (!task?.id || !order?.amount) return false const username = user?.username || user?.name || 'Unknown' @@ -130,11 +132,6 @@ const notifyNewBounty = async (task, order, user) => { }) } -/** - * Helper function to check if a task should trigger Slack notifications - * @param task - The task object (can be Sequelize model instance or plain object) - * @returns boolean - True if task is public and should notify - */ const shouldNotifyForTask = (task) => { if (!task) return false @@ -143,15 +140,7 @@ const shouldNotifyForTask = (task) => { return !(taskData.not_listed === true || taskData.private === true) } -/** - * Helper function to safely send bounty notification with proper error handling - * @param task - The task object - * @param orderData - Order data with amount and currency - * @param user - The user object - * @param context - Optional context string for error logging (e.g., 'wallet payment', 'PayPal payment') - * @returns Promise - True if notification was sent successfully - */ -const notifyBountyWithErrorHandling = async (task, orderData, user, context = 'payment') => { +const notifyBounty = async (task, orderData, user, context = 'payment') => { // Check if task and user exist if (!task || !user) { return false @@ -173,7 +162,7 @@ const notifyBountyWithErrorHandling = async (task, orderData, user, context = 'p } try { - await notifyNewBounty(taskData, order, userData) + await notifyBountyOnSlack(taskData, order, userData) return true } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' @@ -184,7 +173,7 @@ const notifyBountyWithErrorHandling = async (task, orderData, user, context = 'p module.exports = { notifyNewIssue, - notifyNewBounty, + notifyBountyOnSlack, shouldNotifyForTask, - notifyBountyWithErrorHandling + notifyBounty } diff --git a/src/modules/slack/index.ts b/src/modules/shared/slack/index.ts similarity index 75% rename from src/modules/slack/index.ts rename to src/modules/shared/slack/index.ts index e762c31a3..18c3a62e3 100644 --- a/src/modules/slack/index.ts +++ b/src/modules/shared/slack/index.ts @@ -1,10 +1,5 @@ -/** - * Slack notification module - * Handles sending notifications to Slack channel for new issues and bounties - */ - import requestPromise from 'request-promise' -import secrets from '../../config/secrets' +import secrets from '../../../config/secrets' import type { Task, User, OrderData, SlackMessagePayload } from './types' const sendSlackMessage = async (payload: SlackMessagePayload): Promise => { @@ -42,12 +37,6 @@ const formatCurrency = (amount: number | string, currency: string = 'USD'): stri }).format(numAmount) } -/** - * Sends a Slack notification when a new issue is imported - * @param task - The task/issue that was imported - * @param user - The user who imported the issue - * @returns Promise - True if notification was sent successfully - */ export const notifyNewIssue = async ( task: Task | null | undefined, user: User | null | undefined @@ -56,6 +45,10 @@ export const notifyNewIssue = async ( return false } + if (!shouldNotifyForTask(task)) { + return false + } + const username = user?.username || user?.name || 'Unknown' return await sendSlackMessage({ @@ -102,14 +95,7 @@ export const notifyNewIssue = async ( }) } -/** - * Sends a Slack notification when a new bounty payment is completed - * @param task - The task/bounty that received payment - * @param order - The order data containing amount and currency - * @param user - The user who made the payment - * @returns Promise - True if notification was sent successfully - */ -export const notifyNewBounty = async ( +export const notifyBountyOnSlack = async ( task: Task | null | undefined, order: OrderData | null | undefined, user: User | null | undefined @@ -165,11 +151,6 @@ export const notifyNewBounty = async ( }) } -/** - * Helper function to check if a task should trigger Slack notifications - * @param task - The task object (can be Sequelize model instance or plain object) - * @returns boolean - True if task is public and should notify - */ export const shouldNotifyForTask = ( task: Task | { dataValues?: Task } | null | undefined ): boolean => { @@ -182,15 +163,7 @@ export const shouldNotifyForTask = ( return !(taskData.not_listed === true || taskData.private === true) } -/** - * Helper function to safely send bounty notification with proper error handling - * @param task - The task object - * @param orderData - Order data with amount and currency - * @param user - The user object - * @param context - Optional context string for error logging (e.g., 'wallet payment', 'PayPal payment') - * @returns Promise - True if notification was sent successfully - */ -export const notifyBountyWithErrorHandling = async ( +export const notifyBounty = async ( task: Task | { dataValues?: Task } | null | undefined, orderData: OrderData | null | undefined, user: User | { dataValues?: User } | null | undefined, @@ -217,7 +190,7 @@ export const notifyBountyWithErrorHandling = async ( } try { - await notifyNewBounty(taskData, order, userData) + await notifyBountyOnSlack(taskData, order, userData) return true } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' diff --git a/src/modules/slack/types.ts b/src/modules/shared/slack/types.ts similarity index 100% rename from src/modules/slack/types.ts rename to src/modules/shared/slack/types.ts diff --git a/src/modules/tasks/taskBuilds.js b/src/modules/tasks/taskBuilds.js index fed4a3e68..ce61d3a41 100644 --- a/src/modules/tasks/taskBuilds.js +++ b/src/modules/tasks/taskBuilds.js @@ -10,7 +10,7 @@ const userExists = require('../users').userExists // const userOrganizations = require('../users/userOrganizations') const project = require('../projectHelpers') const issueAddedComment = require('../bot/issueAddedComment') -const slack = require('../slack') +const slack = require('../shared/slack') module.exports = Promise.method(async function taskBuilds(taskParameters) { const repoUrl = taskParameters.url @@ -104,12 +104,8 @@ module.exports = Promise.method(async function taskBuilds(taskParameters) { TaskMail.new(userData, taskData) } - // Skip Slack notifications for private or not_listed tasks - const isTaskPublic = !(taskData.not_listed === true || taskData.private === true) - if (isTaskPublic) { - issueAddedComment(task) - slack.notifyNewIssue(taskData, userData) - } + issueAddedComment(task) + slack.notifyNewIssue(taskData, userData) return { ...taskData, ProjectId: taskData.ProjectId } }) diff --git a/src/modules/webhooks/chargeSucceeded.js b/src/modules/webhooks/chargeSucceeded.js index e6ab3ad91..ae78d6ed5 100644 --- a/src/modules/webhooks/chargeSucceeded.js +++ b/src/modules/webhooks/chargeSucceeded.js @@ -1,7 +1,7 @@ const models = require('../../models') const i18n = require('i18n') const SendMail = require('../mail/mail') -const slack = require('../slack') +const slack = require('../shared/slack') const sendEmailSuccess = (event, paid, status, order, req, res) => { return models.User.findOne({ @@ -61,7 +61,7 @@ const updateOrder = async (event, paid, status, req, res) => { amount: orderUpdated.amount, currency: orderUpdated.currency || 'USD' } - await slack.notifyBountyWithErrorHandling( + await slack.notifyBounty( orderUpdated.Task, orderData, orderUpdated.User, diff --git a/src/modules/webhooks/chargeUpdated.js b/src/modules/webhooks/chargeUpdated.js index f7bdd4eea..40b4e2113 100644 --- a/src/modules/webhooks/chargeUpdated.js +++ b/src/modules/webhooks/chargeUpdated.js @@ -2,7 +2,7 @@ const models = require('../../models') const i18n = require('i18n') const moment = require('moment') const SendMail = require('../mail/mail') -const slack = require('../slack') +const slack = require('../shared/slack') module.exports = async function chargeUpdated(event, paid, status, req, res) { if (event?.data?.object?.source?.id) { @@ -52,7 +52,7 @@ module.exports = async function chargeUpdated(event, paid, status, req, res) { amount: orderUpdated.amount, currency: orderUpdated.currency || 'USD' } - await slack.notifyBountyWithErrorHandling( + await slack.notifyBounty( orderUpdated.Task, orderData, orderUpdated.User, diff --git a/src/modules/webhooks/invoicePaymentSucceeded.js b/src/modules/webhooks/invoicePaymentSucceeded.js index faba8fa89..ac0c0f94e 100644 --- a/src/modules/webhooks/invoicePaymentSucceeded.js +++ b/src/modules/webhooks/invoicePaymentSucceeded.js @@ -5,7 +5,7 @@ const SendMail = require('../mail/mail') const WalletMail = require('../mail/wallet') const stripe = require('../shared/stripe/stripe')() const { FAILED_REASON, CURRENCIES, formatStripeAmount } = require('./constants') -const slack = require('../slack') +const slack = require('../shared/slack') module.exports = async function invoicePaymentSucceeded(event, req, res) { return models.User.findOne({ @@ -51,7 +51,7 @@ module.exports = async function invoicePaymentSucceeded(event, req, res) { amount: orderUpdated.amount, currency: orderUpdated.currency || 'USD' } - await slack.notifyBountyWithErrorHandling( + await slack.notifyBounty( orderUpdated.Task, orderData, orderUpdated.User, diff --git a/src/modules/webhooks/invoiceUpdated.js b/src/modules/webhooks/invoiceUpdated.js index 889658744..d1d54ad56 100644 --- a/src/modules/webhooks/invoiceUpdated.js +++ b/src/modules/webhooks/invoiceUpdated.js @@ -5,7 +5,7 @@ const SendMail = require('../mail/mail') const WalletMail = require('../mail/wallet') const stripe = require('../shared/stripe/stripe')() const { FAILED_REASON, CURRENCIES, formatStripeAmount } = require('./constants') -const slack = require('../slack') +const slack = require('../shared/slack') module.exports = async function invoiceUpdated(event, req, res) { // eslint-disable-next-line no-case-declarations @@ -88,7 +88,7 @@ module.exports = async function invoiceUpdated(event, req, res) { amount: orderUpdated.amount, currency: orderUpdated.currency || 'USD' } - await slack.notifyBountyWithErrorHandling( + await slack.notifyBounty( orderUpdated.Task, orderData, orderUpdated.User, diff --git a/test/order.test.js b/test/order.test.js index 917a01ff0..0032301b0 100644 --- a/test/order.test.js +++ b/test/order.test.js @@ -13,7 +13,7 @@ const plan = require('../src/models/plan') const stripe = require('../src/modules/shared/stripe/stripe')() const customerData = require('./data/stripe/stripe.customer') const invoiceData = require('./data/stripe/stripe.invoice.basic') -const { notifyNewBounty } = require('../src/modules/slack') +const { notifyBountyOnSlack } = require('../src/modules/shared/slack') describe('Orders', () => { beforeEach(async () => { @@ -90,7 +90,7 @@ describe('Orders', () => { it('should not call notifyNewBounty when order is created for a task with not_listed set to true', async () => { chai.use(spies) - const slackModule = require('../src/modules/slack') + const slackModule = require('../src/modules/shared/slack') const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') const orderBuilds = require('../src/modules/orders').orderBuilds @@ -120,7 +120,7 @@ describe('Orders', () => { it('should not call notifyNewBounty when order is created for a task with private set to true', async () => { chai.use(spies) - const slackModule = require('../src/modules/slack') + const slackModule = require('../src/modules/shared/slack') const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') const orderBuilds = require('../src/modules/orders').orderBuilds @@ -150,7 +150,7 @@ describe('Orders', () => { it('should not call notifyNewBounty when order is created (notification only on payment completion)', async () => { chai.use(spies) - const slackModule = require('../src/modules/slack') + const slackModule = require('../src/modules/shared/slack') const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') const orderBuilds = require('../src/modules/orders').orderBuilds @@ -182,7 +182,7 @@ describe('Orders', () => { it('should call notifyNewBounty when wallet payment completes for a public task', async () => { chai.use(spies) - const slackModule = require('../src/modules/slack') + const slackModule = require('../src/modules/shared/slack') // Spy on notifyBountyWithErrorHandling since that's what's actually called const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') const orderBuilds = require('../src/modules/orders').orderBuilds @@ -237,7 +237,7 @@ describe('Orders', () => { it('should not call notifyNewBounty when wallet payment completes for a private task', async () => { chai.use(spies) - const slackModule = require('../src/modules/slack') + const slackModule = require('../src/modules/shared/slack') // Spy on notifyBountyWithErrorHandling since that's what's actually called const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') const orderBuilds = require('../src/modules/orders').orderBuilds @@ -940,7 +940,7 @@ describe('Orders', () => { describe('PayPal payment notifications', () => { it('should call notifyNewBounty when PayPal payment completes for a public task', async () => { chai.use(spies) - const slackModule = require('../src/modules/slack') + const slackModule = require('../src/modules/shared/slack') // Spy on notifyBountyWithErrorHandling since that's what's actually called const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') const orderAuthorize = require('../src/modules/orders').orderAuthorize @@ -1011,7 +1011,7 @@ describe('Orders', () => { it('should not call notifyNewBounty when PayPal payment completes for a private task', async () => { chai.use(spies) - const slackModule = require('../src/modules/slack') + const slackModule = require('../src/modules/shared/slack') // Spy on notifyBountyWithErrorHandling since that's what's actually called const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') const orderAuthorize = require('../src/modules/orders').orderAuthorize diff --git a/test/slack.test.js b/test/slack.test.js index ef147b4ad..15c06de22 100644 --- a/test/slack.test.js +++ b/test/slack.test.js @@ -1,6 +1,6 @@ const expect = require('chai').expect const nock = require('nock') -const { notifyNewIssue, notifyNewBounty } = require('../src/modules/slack') +const { notifyNewIssue, notifyBountyOnSlack } = require('../src/modules/shared/slack') describe('Slack Notifications', () => { beforeEach(() => { @@ -38,7 +38,7 @@ describe('Slack Notifications', () => { .post('/services/TEST/WEBHOOK/URL') .reply(200, 'ok') - await notifyNewBounty( + await notifyBountyOnSlack( { id: 1, title: 'Test Task' }, { amount: 100, currency: 'USD' }, { username: 'testuser' } From 63d13027d42ff62ddb64271517adf947d06bd7ea Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Tue, 27 Jan 2026 18:47:18 +0800 Subject: [PATCH 17/20] lint fix --- .claude/settings.local.json | 15 +++++++++++++++ src/modules/orders/orderAuthorize.js | 7 +------ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..a9aef8bc9 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(git mv:*)", + "Bash(git checkout:*)", + "Bash(npm test:*)", + "Bash(npm run test:unit:*)", + "Bash(git branch:*)", + "Bash(git reset:*)", + "Bash(git rm:*)", + "Bash(git commit:*)", + "Bash(git add:*)" + ] + } +} diff --git a/src/modules/orders/orderAuthorize.js b/src/modules/orders/orderAuthorize.js index 51fe68489..762c41311 100644 --- a/src/modules/orders/orderAuthorize.js +++ b/src/modules/orders/orderAuthorize.js @@ -70,12 +70,7 @@ module.exports = Promise.method(function orderAuthorize(orderParameters) { amount: orderData.amount, currency: orderData.currency || 'USD' } - await slack.notifyBounty( - task, - orderDataForNotification, - user, - 'PayPal payment' - ) + await slack.notifyBounty(task, orderDataForNotification, user, 'PayPal payment') } else { PaymentMail.error(user.dataValues, task, orderData.amount) } From 5061ce34009a0a567e3c538641cac9432a79d156 Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Tue, 27 Jan 2026 19:10:59 +0800 Subject: [PATCH 18/20] Remove .claude from git tracking --- .claude/settings.local.json | 15 --------------- .gitignore | 1 + 2 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index a9aef8bc9..000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(git mv:*)", - "Bash(git checkout:*)", - "Bash(npm test:*)", - "Bash(npm run test:unit:*)", - "Bash(git branch:*)", - "Bash(git reset:*)", - "Bash(git rm:*)", - "Bash(git commit:*)", - "Bash(git add:*)" - ] - } -} diff --git a/.gitignore b/.gitignore index f9424c4c5..65241b461 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,4 @@ Thumbs.db # Configuration of database and password/tokens/idSercure .env docker-compose.override.yml +.claude/ From f86c18b68797c9cfa3612f8b5e8389215942fdfe Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Wed, 28 Jan 2026 01:18:58 +0800 Subject: [PATCH 19/20] Fix tests to use renamed slack functions --- test/order.test.js | 36 ++++++++++++++++++------------------ test/slack.test.js | 4 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/test/order.test.js b/test/order.test.js index 0032301b0..1cec08e4f 100644 --- a/test/order.test.js +++ b/test/order.test.js @@ -91,7 +91,7 @@ describe('Orders', () => { it('should not call notifyNewBounty when order is created for a task with not_listed set to true', async () => { chai.use(spies) const slackModule = require('../src/modules/shared/slack') - const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + const slackSpy = chai.spy.on(slackModule, 'notifyBountyOnSlack') const orderBuilds = require('../src/modules/orders').orderBuilds try { @@ -114,14 +114,14 @@ describe('Orders', () => { expect(slackSpy).to.not.have.been.called() } finally { - chai.spy.restore(slackModule, 'notifyNewBounty') + chai.spy.restore(slackModule, 'notifyBountyOnSlack') } }) it('should not call notifyNewBounty when order is created for a task with private set to true', async () => { chai.use(spies) const slackModule = require('../src/modules/shared/slack') - const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + const slackSpy = chai.spy.on(slackModule, 'notifyBountyOnSlack') const orderBuilds = require('../src/modules/orders').orderBuilds try { @@ -144,14 +144,14 @@ describe('Orders', () => { expect(slackSpy).to.not.have.been.called() } finally { - chai.spy.restore(slackModule, 'notifyNewBounty') + chai.spy.restore(slackModule, 'notifyBountyOnSlack') } }) it('should not call notifyNewBounty when order is created (notification only on payment completion)', async () => { chai.use(spies) const slackModule = require('../src/modules/shared/slack') - const slackSpy = chai.spy.on(slackModule, 'notifyNewBounty') + const slackSpy = chai.spy.on(slackModule, 'notifyBountyOnSlack') const orderBuilds = require('../src/modules/orders').orderBuilds try { @@ -176,15 +176,15 @@ describe('Orders', () => { // Notification should NOT be called when order is created, only when payment completes expect(slackSpy).to.not.have.been.called() } finally { - chai.spy.restore(slackModule, 'notifyNewBounty') + chai.spy.restore(slackModule, 'notifyBountyOnSlack') } }) it('should call notifyNewBounty when wallet payment completes for a public task', async () => { chai.use(spies) const slackModule = require('../src/modules/shared/slack') - // Spy on notifyBountyWithErrorHandling since that's what's actually called - const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') + // Spy on notifyBounty since that's what's actually called + const slackSpy = chai.spy.on(slackModule, 'notifyBounty') const orderBuilds = require('../src/modules/orders').orderBuilds try { @@ -231,15 +231,15 @@ describe('Orders', () => { expect(call[1].currency).to.equal('USD') expect(call[3]).to.equal('wallet payment') } finally { - chai.spy.restore(slackModule, 'notifyBountyWithErrorHandling') + chai.spy.restore(slackModule, 'notifyBounty') } }) it('should not call notifyNewBounty when wallet payment completes for a private task', async () => { chai.use(spies) const slackModule = require('../src/modules/shared/slack') - // Spy on notifyBountyWithErrorHandling since that's what's actually called - const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') + // Spy on notifyBounty since that's what's actually called + const slackSpy = chai.spy.on(slackModule, 'notifyBounty') const orderBuilds = require('../src/modules/orders').orderBuilds try { @@ -280,7 +280,7 @@ describe('Orders', () => { // Notification should NOT be called for private tasks expect(slackSpy).to.not.have.been.called() } finally { - chai.spy.restore(slackModule, 'notifyBountyWithErrorHandling') + chai.spy.restore(slackModule, 'notifyBounty') } }) @@ -941,8 +941,8 @@ describe('Orders', () => { it('should call notifyNewBounty when PayPal payment completes for a public task', async () => { chai.use(spies) const slackModule = require('../src/modules/shared/slack') - // Spy on notifyBountyWithErrorHandling since that's what's actually called - const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') + // Spy on notifyBounty since that's what's actually called + const slackSpy = chai.spy.on(slackModule, 'notifyBounty') const orderAuthorize = require('../src/modules/orders').orderAuthorize // Mock PayPal API responses @@ -1004,7 +1004,7 @@ describe('Orders', () => { expect(call[1].currency).to.equal('USD') expect(call[3]).to.equal('PayPal payment') } finally { - chai.spy.restore(slackModule, 'notifyBountyWithErrorHandling') + chai.spy.restore(slackModule, 'notifyBounty') nock.cleanAll() } }) @@ -1012,8 +1012,8 @@ describe('Orders', () => { it('should not call notifyNewBounty when PayPal payment completes for a private task', async () => { chai.use(spies) const slackModule = require('../src/modules/shared/slack') - // Spy on notifyBountyWithErrorHandling since that's what's actually called - const slackSpy = chai.spy.on(slackModule, 'notifyBountyWithErrorHandling') + // Spy on notifyBounty since that's what's actually called + const slackSpy = chai.spy.on(slackModule, 'notifyBounty') const orderAuthorize = require('../src/modules/orders').orderAuthorize // Mock PayPal API responses @@ -1063,7 +1063,7 @@ describe('Orders', () => { // Notification should NOT be called for private tasks expect(slackSpy).to.not.have.been.called() } finally { - chai.spy.restore(slackModule, 'notifyNewBounty') + chai.spy.restore(slackModule, 'notifyBountyOnSlack') nock.cleanAll() } }) diff --git a/test/slack.test.js b/test/slack.test.js index 15c06de22..c7b1a89cd 100644 --- a/test/slack.test.js +++ b/test/slack.test.js @@ -48,8 +48,8 @@ describe('Slack Notifications', () => { }) it('should handle missing order data gracefully', async () => { - await notifyNewBounty({ id: 1, title: 'Test' }, null, { username: 'test' }) - await notifyNewBounty({ id: 1, title: 'Test' }, {}, { username: 'test' }) + await notifyBountyOnSlack({ id: 1, title: 'Test' }, null, { username: 'test' }) + await notifyBountyOnSlack({ id: 1, title: 'Test' }, {}, { username: 'test' }) }) }) }) \ No newline at end of file From d56e918df30180bd2f557a25d44529b6bdd0e3aa Mon Sep 17 00:00:00 2001 From: devmaster-x Date: Wed, 28 Jan 2026 03:09:41 +0800 Subject: [PATCH 20/20] Fix private task tests to spy on notifyBountyOnSlack --- test/order.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/order.test.js b/test/order.test.js index 1cec08e4f..eff3dbb7e 100644 --- a/test/order.test.js +++ b/test/order.test.js @@ -238,8 +238,8 @@ describe('Orders', () => { it('should not call notifyNewBounty when wallet payment completes for a private task', async () => { chai.use(spies) const slackModule = require('../src/modules/shared/slack') - // Spy on notifyBounty since that's what's actually called - const slackSpy = chai.spy.on(slackModule, 'notifyBounty') + // Spy on notifyBountyOnSlack - the internal function that sends to Slack + const slackSpy = chai.spy.on(slackModule, 'notifyBountyOnSlack') const orderBuilds = require('../src/modules/orders').orderBuilds try { @@ -277,10 +277,10 @@ describe('Orders', () => { taskId: task.id }) - // Notification should NOT be called for private tasks + // Notification should NOT be sent to Slack for private tasks expect(slackSpy).to.not.have.been.called() } finally { - chai.spy.restore(slackModule, 'notifyBounty') + chai.spy.restore(slackModule, 'notifyBountyOnSlack') } }) @@ -1012,8 +1012,8 @@ describe('Orders', () => { it('should not call notifyNewBounty when PayPal payment completes for a private task', async () => { chai.use(spies) const slackModule = require('../src/modules/shared/slack') - // Spy on notifyBounty since that's what's actually called - const slackSpy = chai.spy.on(slackModule, 'notifyBounty') + // Spy on notifyBountyOnSlack - the internal function that sends to Slack + const slackSpy = chai.spy.on(slackModule, 'notifyBountyOnSlack') const orderAuthorize = require('../src/modules/orders').orderAuthorize // Mock PayPal API responses @@ -1060,7 +1060,7 @@ describe('Orders', () => { PayerID: 'TEST_PAYER_ID' }) - // Notification should NOT be called for private tasks + // Notification should NOT be sent to Slack for private tasks expect(slackSpy).to.not.have.been.called() } finally { chai.spy.restore(slackModule, 'notifyBountyOnSlack')