From ce54a1e730e17c35aaf1229870dcf90132bcd0d4 Mon Sep 17 00:00:00 2001 From: Yuyi5la Date: Sat, 31 May 2025 15:25:04 +0100 Subject: [PATCH 1/4] ella --- __tests__/currencyRoutes.test.js | 2 +- __tests__/transactionService.test.js | 113 ++++++++++++++++++ package-lock.json | 142 +++++++++++------------ package.json | 3 +- src/controllers/transactionController.js | 56 +++++++++ src/index.js | 2 + src/routes/transactionRoutes.js | 16 +++ 7 files changed, 261 insertions(+), 73 deletions(-) create mode 100644 __tests__/transactionService.test.js create mode 100644 src/controllers/transactionController.js create mode 100644 src/routes/transactionRoutes.js diff --git a/__tests__/currencyRoutes.test.js b/__tests__/currencyRoutes.test.js index 13460dc..4715600 100644 --- a/__tests__/currencyRoutes.test.js +++ b/__tests__/currencyRoutes.test.js @@ -135,4 +135,4 @@ describe('Currency Routes', () => { expect(mockedCurrencyService.convertCurrency).not.toHaveBeenCalled(); }); }); -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/__tests__/transactionService.test.js b/__tests__/transactionService.test.js new file mode 100644 index 0000000..eba81d7 --- /dev/null +++ b/__tests__/transactionService.test.js @@ -0,0 +1,113 @@ +// import { jest } from '@jest/globals'; +// import request from 'supertest'; +// import express from 'express'; +// import transactionRoutes from '../src/routes/transactionRoutes.js'; + + +// jest.mock('../src/models/transaction.js', () => ({ +// __esModule: true, +// default: { +// create: jest.fn(), +// findAll: jest.fn(), +// findByPk: jest.fn(), +// }, +// })); + +// const app = express(); +// app.use(express.json()); +// app.use('/api/transactions', transactionRoutes); + +// describe('Transaction Routes', () => { +// beforeEach(() => { +// jest.clearAllMocks(); +// }); + +// describe('POST /api/transactions', () => { +// it('should create a transaction successfully', async () => { +// const mockTransaction = { id: 1, amount: 100, transaction: 'USD' }; + +// transactionModel.create.mockResolvedValue(mockTransaction); + +// const response = await request(app) +// .post('/api/transactions') +// .send({ amount: 100, transaction: 'USD' }) +// .expect('Content-Type', /json/) +// .expect(201); + +// expect(response.body).toEqual(mockTransaction); +// expect(transactionModel.create).toHaveBeenCalledWith({ amount: 100, transaction: 'USD' }); +// }); +// }); + +// describe('GET /api/transactions', () => { +// it('should fetch all transactions', async () => { +// const mockTransactions = [{ id: 1, amount: 100, transaction: 'USD' }]; + +// transactionModel.findAll.mockResolvedValue(mockTransactions); + +// const response = await request(app) +// .get('/api/transactions') +// .expect('Content-Type', /json/) +// .expect(200); + +// expect(response.body).toEqual(mockTransactions); +// expect(transactionModel.findAll).toHaveBeenCalled(); +// }); +// }); + +// describe('GET /api/transactions/:id', () => { +// it('should return a transaction by id', async () => { +// const mockTransaction = { id: 1, amount: 100, transaction: 'USD' }; + +// transactionModel.findByPk.mockResolvedValue(mockTransaction); + +// const response = await request(app) +// .get('/api/transactions/1') +// .expect('Content-Type', /json/) +// .expect(200); + +// expect(response.body).toEqual(mockTransaction); +// expect(transactionModel.findByPk).toHaveBeenCalledWith('1'); +// }); + +// it('should return 404 if transaction not found', async () => { +// transactionModel.findByPk.mockResolvedValue(null); + +// const response = await request(app) +// .get('/api/transactions/999') +// .expect('Content-Type', /json/) +// .expect(404); + +// expect(response.body).toEqual({ error: 'Transaction not found' }); +// expect(transactionModel.findByPk).toHaveBeenCalledWith('999'); +// }); +// }); +// }); +// transactions.test.js + +import { jest } from '@jest/globals'; + + +// Mock the entire service module +jest.mock(process.cwd() + '/src/controllers/transactionController.js', () => { + const mockService = { + createTransaction: jest.fn(), + getTransactionById: jest.fn(), + updateTransactionStatus: jest.fn(), + getAllTransactions:jest.fn() + + + }; + return { + __esModule: true, + default: mockService + }; +}); + +import transactionService from '../src/controllers/transactionController.js'; + +// Ensure the methods are Jest mocks +transactionService.createTransaction = jest.fn(); +transactionService.getTransactionById = jest.fn(); +transactionService.updateTransactionStatus = jest.fn(); +transactionService.getAllTransactions = jest.fn(); diff --git a/package-lock.json b/package-lock.json index 4fb9778..825db7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,9 +72,9 @@ } }, "node_modules/@babel/core": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.3.tgz", - "integrity": "sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, "license": "MIT", "dependencies": { @@ -83,10 +83,10 @@ "@babel/generator": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.3", - "@babel/parser": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.3", + "@babel/traverse": "^7.27.4", "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -209,9 +209,9 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.3.tgz", - "integrity": "sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", + "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -223,9 +223,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz", - "integrity": "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz", + "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==", "dev": true, "license": "MIT", "dependencies": { @@ -493,15 +493,15 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.3.tgz", - "integrity": "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.3", + "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", @@ -1548,28 +1548,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -1604,22 +1582,6 @@ "node": ">=8" } }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", @@ -1647,23 +1609,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3727,6 +3672,61 @@ } } }, + "node_modules/jest-config/node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/jest-config/node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", diff --git a/package.json b/package.json index de6d61d..45290a3 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "zod": "^3.25.30" }, "devDependencies": { - "cross-env": "^7.0.3", + + "cross-env": "^7.0.3", "eslint": "^9.27.0", "jest": "^29.7.0", "nodemon": "^3.1.10", diff --git a/src/controllers/transactionController.js b/src/controllers/transactionController.js new file mode 100644 index 0000000..5205c4f --- /dev/null +++ b/src/controllers/transactionController.js @@ -0,0 +1,56 @@ +import { transaction } from "../models/transaction.js"; + +// src/controllers/transactionController.js + +const { transaction } = require('../models'); + +exports.createTransaction = async (req, res) => { + try { + const txn = await transaction.create(req.body); + res.status(201).json({ success: true, data: txn }); + } catch (error) { + console.error("Create Transaction Error:", error.message); + res.status(500).json({ success: false, message: error.message }); + } +}; + +exports.getAllTransactions = async (req, res) => { + try { + const txns = await transaction.findAll(); + res.status(200).json({ success: true, data: txns }); + } catch (error) { + console.error("Fetch Transactions Error:", error.message); + res.status(500).json({ success: false, message: error.message }); + } +}; + +exports.getTransactionById = async (req, res) => { + try { + const txn = await transaction.findByPk(req.params.id); + if (!txn) { + return res.status(404).json({ success: false, message: "Transaction not found" }); + } + res.status(200).json({ success: true, data: txn }); + } catch (error) { + console.error("Get Transaction Error:", error.message); + res.status(500).json({ success: false, message: error.message }); + } +}; + +exports.updateTransactionStatus = async (req, res) => { + try { + const txn = await transaction.findByPk(req.params.id); + if (!txn) { + return res.status(404).json({ success: false, message: "Transaction not found" }); + } + + txn.txn_status = req.body.txn_status; + txn.confirmed_at = req.body.confirmed_at; + await txn.save(); + + res.status(200).json({ success: true, data: txn }); + } catch (error) { + console.error("Update Transaction Error:", error.message); + res.status(500).json({ success: false, message: error.message }); + } +}; diff --git a/src/index.js b/src/index.js index 1d349d7..969fc3b 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ import compression from 'compression'; import walletRoute from './routes/walletRoute.js'; import { customersRouter } from './routes/customersRoutes.js'; import currencyRoutes from './routes/currencyRoutes.js'; +import transactionRoutes from './routes/transactionRoutes.js'; // Config and DB import { config } from './configs/config.env.js'; @@ -42,6 +43,7 @@ app.use(urlencoded({ extended: true, limit: '200mb' })); app.use('/api/wallet', walletRoute); app.use(`${BASE_URL}/customers`, customersRouter); app.use(`${BASE_URL}/currency`, currencyRoutes); +app.use(`${BASE_URL}/transactions`, transactionRoutes); // Error handling middleware app.use((error, _req, res) => { diff --git a/src/routes/transactionRoutes.js b/src/routes/transactionRoutes.js new file mode 100644 index 0000000..cdbc3d9 --- /dev/null +++ b/src/routes/transactionRoutes.js @@ -0,0 +1,16 @@ +import express from "express"; +import { + createTransaction, + getAllTransactions, + getTransactionById, + updateTransactionStatus +} from "../controllers/transactionController.js"; + +const router = express.Router(); + +router.post("/", createTransaction); +router.get("/", getAllTransactions); +router.get("/:txn_id", getTransactionById); +router.put("/:txn_id", updateTransactionStatus); + +export default router; From 41ccc3012b896188d23600692f03f16372d4bab0 Mon Sep 17 00:00:00 2001 From: Yuyi5la Date: Sat, 31 May 2025 18:00:13 +0100 Subject: [PATCH 2/4] updated transaction module with tests --- __tests__/transactionRoutes.test.js | 165 ++++++++++++++ __tests__/transactionService.test.js | 264 +++++++++++++++-------- src/controllers/transactionController.js | 98 ++++++--- src/models/transaction.js | 3 +- src/routes/transactionRoutes.js | 14 +- src/services/transactionService.js | 101 +++++++++ 6 files changed, 509 insertions(+), 136 deletions(-) create mode 100644 __tests__/transactionRoutes.test.js create mode 100644 src/services/transactionService.js diff --git a/__tests__/transactionRoutes.test.js b/__tests__/transactionRoutes.test.js new file mode 100644 index 0000000..7aa88ea --- /dev/null +++ b/__tests__/transactionRoutes.test.js @@ -0,0 +1,165 @@ +import { jest } from '@jest/globals'; +import request from 'supertest'; +import express from 'express'; +import transactionRoutes from '../src/routes/transactionRoutes.js'; +import * as transactionServiceModule from '../src/services/transactionService.js'; + +const mockedTransactionService = transactionServiceModule.default; + +// Mock service methods +mockedTransactionService.createTransaction = jest.fn(); +mockedTransactionService.getAllTransactions = jest.fn(); +mockedTransactionService.getTransactionById = jest.fn(); +mockedTransactionService.updateTransactionStatus = jest.fn(); + +let app; +let server; + +beforeAll(() => { + app = express(); + app.use(express.json()); + app.use('/api/transactions', transactionRoutes); + server = app.listen(0); +}); + +afterAll(async () => { + await new Promise((resolve) => server.close(resolve)); +}); + +describe('Transaction Routes', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('POST /api/transactions', () => { + it('should create a new transaction successfully', async () => { + const validTransactionData = { + amount: 100, + currency: 'USD', + sender_id: '123e4567-e89b-12d3-a456-426614174000', + receiver_id: '123e4567-e89b-12d3-a456-426614174001', + txn_type: 'transfer' + }; + + const mockResponse = { + success: true, + data: { + ...validTransactionData, + id: 1, + txn_status: 'pending', + createdAt: '2023-01-01T00:00:00.000Z' + } + }; + + mockedTransactionService.createTransaction.mockResolvedValue(mockResponse); + + const response = await request(app) + .post('/api/transactions') + .send(validTransactionData) + .expect('Content-Type', /json/) + .expect(201); + + expect(response.body).toEqual(mockResponse); + expect(mockedTransactionService.createTransaction).toHaveBeenCalledWith(validTransactionData); + }); + + it('should return 400 for invalid transaction data', async () => { + const invalidTransactionData = { + amount: 'not-a-number', + currency: 'US', + sender_id: 'invalid', + txn_type: 'invalid-type' + }; + + const response = await request(app) + .post('/api/transactions') + .send(invalidTransactionData) + .expect('Content-Type', /json/) + .expect(400); + + expect(response.body).toHaveProperty('error'); + expect(response.body).toHaveProperty('details'); + expect(mockedTransactionService.createTransaction).not.toHaveBeenCalled(); + }); + }); + + describe('GET /api/transactions', () => { + it('should fetch all transactions successfully', async () => { + const mockResponse = { + success: true, + data: [ + { id: 1, amount: 100, currency: 'USD' }, + { id: 2, amount: 200, currency: 'EUR' } + ] + }; + + mockedTransactionService.getAllTransactions.mockResolvedValue(mockResponse); + + const response = await request(app) + .get('/api/transactions') + .expect('Content-Type', /json/) + .expect(200); + + expect(response.body).toEqual(mockResponse); + expect(mockedTransactionService.getAllTransactions).toHaveBeenCalled(); + }); + + it('should return empty array when no transactions exist', async () => { + const mockResponse = { + success: true, + data: [] + }; + + mockedTransactionService.getAllTransactions.mockResolvedValue(mockResponse); + + const response = await request(app) + .get('/api/transactions') + .expect('Content-Type', /json/) + .expect(200); + + expect(response.body.data).toEqual([]); + }); + }); + + describe('GET /api/transactions/:id', () => { + it('should fetch a single transaction by ID', async () => { + const mockResponse = { + success: true, + data: { + id: 1, + amount: 100, + currency: 'USD', + txn_status: 'completed' + } + }; + + mockedTransactionService.getTransactionById.mockResolvedValue(mockResponse); + + const response = await request(app) + .get('/api/transactions/1') + .expect('Content-Type', /json/) + .expect(200); + + expect(response.body).toEqual(mockResponse); + expect(mockedTransactionService.getTransactionById).toHaveBeenCalledWith('1'); + }); + + it('should return 404 for non-existent transaction', async () => { + const mockResponse = { + success: false, + error: 'Transaction not found', + status: 404 + }; + + mockedTransactionService.getTransactionById.mockResolvedValue(mockResponse); + + const response = await request(app) + .get('/api/transactions/999') + .expect('Content-Type', /json/) + .expect(404); + + expect(response.body.error).toBe('Transaction not found'); + }); + }); + +}); \ No newline at end of file diff --git a/__tests__/transactionService.test.js b/__tests__/transactionService.test.js index eba81d7..c6c57ca 100644 --- a/__tests__/transactionService.test.js +++ b/__tests__/transactionService.test.js @@ -1,102 +1,12 @@ -// import { jest } from '@jest/globals'; -// import request from 'supertest'; -// import express from 'express'; -// import transactionRoutes from '../src/routes/transactionRoutes.js'; - - -// jest.mock('../src/models/transaction.js', () => ({ -// __esModule: true, -// default: { -// create: jest.fn(), -// findAll: jest.fn(), -// findByPk: jest.fn(), -// }, -// })); - -// const app = express(); -// app.use(express.json()); -// app.use('/api/transactions', transactionRoutes); - -// describe('Transaction Routes', () => { -// beforeEach(() => { -// jest.clearAllMocks(); -// }); - -// describe('POST /api/transactions', () => { -// it('should create a transaction successfully', async () => { -// const mockTransaction = { id: 1, amount: 100, transaction: 'USD' }; - -// transactionModel.create.mockResolvedValue(mockTransaction); - -// const response = await request(app) -// .post('/api/transactions') -// .send({ amount: 100, transaction: 'USD' }) -// .expect('Content-Type', /json/) -// .expect(201); - -// expect(response.body).toEqual(mockTransaction); -// expect(transactionModel.create).toHaveBeenCalledWith({ amount: 100, transaction: 'USD' }); -// }); -// }); - -// describe('GET /api/transactions', () => { -// it('should fetch all transactions', async () => { -// const mockTransactions = [{ id: 1, amount: 100, transaction: 'USD' }]; - -// transactionModel.findAll.mockResolvedValue(mockTransactions); - -// const response = await request(app) -// .get('/api/transactions') -// .expect('Content-Type', /json/) -// .expect(200); - -// expect(response.body).toEqual(mockTransactions); -// expect(transactionModel.findAll).toHaveBeenCalled(); -// }); -// }); - -// describe('GET /api/transactions/:id', () => { -// it('should return a transaction by id', async () => { -// const mockTransaction = { id: 1, amount: 100, transaction: 'USD' }; - -// transactionModel.findByPk.mockResolvedValue(mockTransaction); - -// const response = await request(app) -// .get('/api/transactions/1') -// .expect('Content-Type', /json/) -// .expect(200); - -// expect(response.body).toEqual(mockTransaction); -// expect(transactionModel.findByPk).toHaveBeenCalledWith('1'); -// }); - -// it('should return 404 if transaction not found', async () => { -// transactionModel.findByPk.mockResolvedValue(null); - -// const response = await request(app) -// .get('/api/transactions/999') -// .expect('Content-Type', /json/) -// .expect(404); - -// expect(response.body).toEqual({ error: 'Transaction not found' }); -// expect(transactionModel.findByPk).toHaveBeenCalledWith('999'); -// }); -// }); -// }); -// transactions.test.js - import { jest } from '@jest/globals'; - // Mock the entire service module -jest.mock(process.cwd() + '/src/controllers/transactionController.js', () => { +jest.mock(process.cwd() + '/src/services/transactionService.js', () => { const mockService = { createTransaction: jest.fn(), + getAllTransactions: jest.fn(), getTransactionById: jest.fn(), - updateTransactionStatus: jest.fn(), - getAllTransactions:jest.fn() - - + updateTransactionStatus: jest.fn() }; return { __esModule: true, @@ -104,10 +14,174 @@ jest.mock(process.cwd() + '/src/controllers/transactionController.js', () => { }; }); -import transactionService from '../src/controllers/transactionController.js'; +import transactionService from '../src/services/transactionService.js'; // Ensure the methods are Jest mocks transactionService.createTransaction = jest.fn(); +transactionService.getAllTransactions = jest.fn(); transactionService.getTransactionById = jest.fn(); transactionService.updateTransactionStatus = jest.fn(); -transactionService.getAllTransactions = jest.fn(); + +describe('TransactionService', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createTransaction', () => { + it('should successfully create a transaction', async () => { + const mockTransactionData = { + amount: 100, + currency: 'USD', + sender_id: 'sender123', + receiver_id: 'receiver456', + txn_type: 'transfer' + }; + + const mockResponse = { + success: true, + data: { + ...mockTransactionData, + id: 1, + txn_status: 'pending', + createdAt: '2023-01-01T00:00:00.000Z' + } + }; + + transactionService.createTransaction.mockResolvedValue(mockResponse); + + const result = await transactionService.createTransaction(mockTransactionData); + + expect(result.success).toBe(true); + expect(result.data).toEqual(mockResponse.data); + expect(transactionService.createTransaction).toHaveBeenCalledWith(mockTransactionData); + }); + + it('should handle creation errors', async () => { + const mockError = { + success: false, + error: 'Failed to create transaction' + }; + + transactionService.createTransaction.mockResolvedValue(mockError); + + const result = await transactionService.createTransaction({}); + + expect(result.success).toBe(false); + expect(result.error).toBe('Failed to create transaction'); + }); + }); + + describe('getAllTransactions', () => { + it('should fetch all transactions successfully', async () => { + const mockResponse = { + success: true, + data: [ + { id: 1, amount: 100 }, + { id: 2, amount: 200 } + ] + }; + + transactionService.getAllTransactions.mockResolvedValue(mockResponse); + + const result = await transactionService.getAllTransactions(); + + expect(result.success).toBe(true); + expect(result.data.length).toBe(2); + expect(transactionService.getAllTransactions).toHaveBeenCalled(); + }); + + it('should handle fetch errors', async () => { + const mockError = { + success: false, + error: 'Failed to fetch transactions' + }; + + transactionService.getAllTransactions.mockResolvedValue(mockError); + + const result = await transactionService.getAllTransactions(); + + expect(result.success).toBe(false); + expect(result.error).toBe('Failed to fetch transactions'); + }); + }); + + describe('getTransactionById', () => { + it('should fetch a transaction by ID successfully', async () => { + const mockResponse = { + success: true, + data: { + id: 1, + amount: 100, + currency: 'USD' + } + }; + + transactionService.getTransactionById.mockResolvedValue(mockResponse); + + const result = await transactionService.getTransactionById(1); + + expect(result.success).toBe(true); + expect(result.data.id).toBe(1); + expect(transactionService.getTransactionById).toHaveBeenCalledWith(1); + }); + + it('should handle transaction not found', async () => { + const mockError = { + success: false, + error: 'Transaction not found', + status: 404 + }; + + transactionService.getTransactionById.mockResolvedValue(mockError); + + const result = await transactionService.getTransactionById(999); + + expect(result.success).toBe(false); + expect(result.error).toBe('Transaction not found'); + expect(result.status).toBe(404); + }); + }); + + describe('updateTransactionStatus', () => { + it('should update transaction status successfully', async () => { + const mockResponse = { + success: true, + data: { + id: 1, + txn_status: 'completed', + confirmed_at: '2023-01-01T00:00:00.000Z' + } + }; + + transactionService.updateTransactionStatus.mockResolvedValue(mockResponse); + + const result = await transactionService.updateTransactionStatus(1, { + txn_status: 'completed', + confirmed_at: '2023-01-01T00:00:00.000Z' + }); + + expect(result.success).toBe(true); + expect(result.data.txn_status).toBe('completed'); + expect(transactionService.updateTransactionStatus).toHaveBeenCalledWith(1, { + txn_status: 'completed', + confirmed_at: '2023-01-01T00:00:00.000Z' + }); + }); + + it('should handle transaction not found during update', async () => { + const mockError = { + success: false, + error: 'Transaction not found', + status: 404 + }; + + transactionService.updateTransactionStatus.mockResolvedValue(mockError); + + const result = await transactionService.updateTransactionStatus(999, {}); + + expect(result.success).toBe(false); + expect(result.error).toBe('Transaction not found'); + expect(result.status).toBe(404); + }); + }); +}); diff --git a/src/controllers/transactionController.js b/src/controllers/transactionController.js index 5205c4f..6b74a3a 100644 --- a/src/controllers/transactionController.js +++ b/src/controllers/transactionController.js @@ -1,56 +1,88 @@ -import { transaction } from "../models/transaction.js"; +import { z } from 'zod'; +import transactionService from '../services/transactionService.js'; -// src/controllers/transactionController.js +const transactionCreateSchema = z.object({ + amount: z.number().positive(), + currency: z.string().min(3).max(3), + sender_id: z.string().uuid(), + receiver_id: z.string().uuid(), + txn_type: z.enum(['deposit', 'withdrawal', 'transfer']) +}); -const { transaction } = require('../models'); +const transactionUpdateSchema = z.object({ + txn_status: z.enum(['pending', 'completed', 'failed']), + confirmed_at: z.date().optional() +}); -exports.createTransaction = async (req, res) => { +export const createTransaction = async (req, res) => { try { - const txn = await transaction.create(req.body); - res.status(201).json({ success: true, data: txn }); + const validationResult = transactionCreateSchema.safeParse(req.body); + if (!validationResult.success) { + return res.status(400).json({ + success: false, + error: 'Invalid transaction data', + details: validationResult.error.errors + }); + } + + const result = await transactionService.createTransaction(req.body); + return res.status(result.success ? 201 : 400).json(result); } catch (error) { - console.error("Create Transaction Error:", error.message); - res.status(500).json({ success: false, message: error.message }); + return res.status(500).json({ + success: false, + error: 'Internal server error', + message: error.message + }); } }; -exports.getAllTransactions = async (req, res) => { +export const getAllTransactions = async (req, res) => { try { - const txns = await transaction.findAll(); - res.status(200).json({ success: true, data: txns }); + const result = await transactionService.getAllTransactions(); + return res.status(result.success ? 200 : 400).json(result); } catch (error) { - console.error("Fetch Transactions Error:", error.message); - res.status(500).json({ success: false, message: error.message }); + return res.status(500).json({ + success: false, + error: 'Internal server error', + message: error.message + }); } }; -exports.getTransactionById = async (req, res) => { +export const getTransactionById = async (req, res) => { try { - const txn = await transaction.findByPk(req.params.id); - if (!txn) { - return res.status(404).json({ success: false, message: "Transaction not found" }); - } - res.status(200).json({ success: true, data: txn }); + const result = await transactionService.getTransactionById(req.params.id); + return res.status(result.success ? 200 : (result.status || 400)).json(result); } catch (error) { - console.error("Get Transaction Error:", error.message); - res.status(500).json({ success: false, message: error.message }); + return res.status(500).json({ + success: false, + error: 'Internal server error', + message: error.message + }); } }; -exports.updateTransactionStatus = async (req, res) => { +export const updateTransactionStatus = async (req, res) => { try { - const txn = await transaction.findByPk(req.params.id); - if (!txn) { - return res.status(404).json({ success: false, message: "Transaction not found" }); + const validationResult = transactionUpdateSchema.safeParse(req.body); + if (!validationResult.success) { + return res.status(400).json({ + success: false, + error: 'Invalid status data', + details: validationResult.error.errors + }); } - txn.txn_status = req.body.txn_status; - txn.confirmed_at = req.body.confirmed_at; - await txn.save(); - - res.status(200).json({ success: true, data: txn }); + const result = await transactionService.updateTransactionStatus( + req.params.id, + req.body + ); + return res.status(result.success ? 200 : (result.status || 400)).json(result); } catch (error) { - console.error("Update Transaction Error:", error.message); - res.status(500).json({ success: false, message: error.message }); + return res.status(500).json({ + success: false, + error: 'Internal server error', + message: error.message + }); } -}; +}; \ No newline at end of file diff --git a/src/models/transaction.js b/src/models/transaction.js index ae249b4..7765b34 100644 --- a/src/models/transaction.js +++ b/src/models/transaction.js @@ -54,4 +54,5 @@ export const transaction = sequelize.define('transaction', { }, { timestamps: true, tableName: 'transaction' -}); \ No newline at end of file +}); +export default transaction; \ No newline at end of file diff --git a/src/routes/transactionRoutes.js b/src/routes/transactionRoutes.js index cdbc3d9..ac8534d 100644 --- a/src/routes/transactionRoutes.js +++ b/src/routes/transactionRoutes.js @@ -1,16 +1,16 @@ -import express from "express"; +import express from 'express'; import { createTransaction, getAllTransactions, getTransactionById, updateTransactionStatus -} from "../controllers/transactionController.js"; +} from '../controllers/transactionController.js'; const router = express.Router(); -router.post("/", createTransaction); -router.get("/", getAllTransactions); -router.get("/:txn_id", getTransactionById); -router.put("/:txn_id", updateTransactionStatus); +router.post('/', createTransaction); +router.get('/', getAllTransactions); +router.get('/:id', getTransactionById); +router.patch('/:id/status', updateTransactionStatus); -export default router; +export default router; \ No newline at end of file diff --git a/src/services/transactionService.js b/src/services/transactionService.js new file mode 100644 index 0000000..d58e6ac --- /dev/null +++ b/src/services/transactionService.js @@ -0,0 +1,101 @@ +import BaseBitnobService from './baseBitnobService.js'; +import { transaction } from "../models/transaction.js"; + +class TransactionService extends BaseBitnobService { + async createTransaction(transactionData) { + try { + this.logRequest('/transactions', transactionData); + + const txn = await transaction.create(transactionData); + + return { + success: true, + data: txn + }; + } catch (error) { + this.logError(error); + return { + success: false, + error: error.message || 'Failed to create transaction' + }; + } + } + + async getAllTransactions() { + try { + this.logRequest('/transactions'); + + const txns = await transaction.findAll(); + + return { + success: true, + data: txns + }; + } catch (error) { + this.logError(error); + return { + success: false, + error: error.message || 'Failed to fetch transactions' + }; + } + } + + async getTransactionById(id) { + try { + this.logRequest(`/transactions/${id}`, { id }); + + const txn = await transaction.findByPk(id); + + if (!txn) { + return { + success: false, + error: 'Transaction not found', + status: 404 + }; + } + + return { + success: true, + data: txn + }; + } catch (error) { + this.logError(error); + return { + success: false, + error: error.message || 'Failed to get transaction' + }; + } + } + + async updateTransactionStatus(id, statusData) { + try { + this.logRequest(`/transactions/${id}/status`, { id, ...statusData }); + + const txn = await transaction.findByPk(id); + + if (!txn) { + return { + success: false, + error: 'Transaction not found', + status: 404 + }; + } + + txn.txn_status = statusData.txn_status; + txn.confirmed_at = statusData.confirmed_at; + await txn.save(); + + return { + success: true, + data: txn + }; + } catch (error) { + this.logError(error); + return { + success: false, + error: error.message || 'Failed to update transaction status' + }; + } + } +} +export default new TransactionService(); \ No newline at end of file From 09ef7e4c73ea061d2c8dc959a8b29f76fb6b8b6b Mon Sep 17 00:00:00 2001 From: Yuyi5la Date: Sat, 31 May 2025 18:36:29 +0100 Subject: [PATCH 3/4] updated wallet module and test --- __tests__/walletRoutes.test.js | 95 +++++++++++++ __tests__/walletService.test.js | 132 ++++++++++++++++++ src/controllers/wallet.js | 201 ---------------------------- src/controllers/walletController.js | 68 ++++++++++ src/index.js | 2 +- src/routes/walletRoute.js | 15 --- src/routes/walletRoutes.js | 14 ++ src/services/walletService.js | 81 +++++++++++ 8 files changed, 391 insertions(+), 217 deletions(-) create mode 100644 __tests__/walletRoutes.test.js create mode 100644 __tests__/walletService.test.js delete mode 100644 src/controllers/wallet.js create mode 100644 src/controllers/walletController.js delete mode 100644 src/routes/walletRoute.js create mode 100644 src/routes/walletRoutes.js create mode 100644 src/services/walletService.js diff --git a/__tests__/walletRoutes.test.js b/__tests__/walletRoutes.test.js new file mode 100644 index 0000000..018e8e3 --- /dev/null +++ b/__tests__/walletRoutes.test.js @@ -0,0 +1,95 @@ +import { jest } from '@jest/globals'; +import request from 'supertest'; +import express from 'express'; +import walletRoutes from '../src/routes/walletRoutes.js'; +import * as walletServiceModule from '../src/services/walletService.js'; + +const mockedWalletService = walletServiceModule.default; + + +mockedWalletService.createWallet = jest.fn(); +mockedWalletService.getAllWallets = jest.fn(); +mockedWalletService.getWalletByCoin = jest.fn(); + +let app; +let server; + +beforeAll(() => { + app = express(); + app.use(express.json()); + app.use('/api/wallets', walletRoutes); + server = app.listen(0); // Use port 0 for random available port +}); + +afterAll(async () => { + await new Promise((resolve) => server.close(resolve)); +}); + +describe('Wallet Routes', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('POST /api/wallets', () => { + it('should create a new wallet successfully', async () => { + const mockResponse = { + success: true, + data: { + id: 'wallet_123', + coin: 'trx', + address: 'TXYZ1234567890', + status: 'active' + } + }; + mockedWalletService.createWallet.mockResolvedValue(mockResponse); + + const response = await request(app) + .post('/api/wallets') + .send({ coin: 'trx' }) + .expect('Content-Type', /json/) + .expect(201); + + expect(response.body).toEqual(mockResponse); + expect(mockedWalletService.createWallet).toHaveBeenCalledWith('trx'); + }); + + }); + + describe('GET /api/wallets', () => { + it('should fetch all wallets successfully', async () => { + const mockResponse = { + success: true, + data: [ + { id: 'wallet_1', coin: 'trx' }, + { id: 'wallet_2', coin: 'bnb' } + ] + }; + mockedWalletService.getAllWallets.mockResolvedValue(mockResponse); + + const response = await request(app) + .get('/api/wallets') + .expect('Content-Type', /json/) + .expect(200); + + expect(response.body).toEqual(mockResponse); + expect(mockedWalletService.getAllWallets).toHaveBeenCalled(); + }); + + it('should handle empty wallet list', async () => { + const mockResponse = { + success: true, + data: [] + }; + mockedWalletService.getAllWallets.mockResolvedValue(mockResponse); + + const response = await request(app) + .get('/api/wallets') + .expect('Content-Type', /json/) + .expect(200); + + expect(response.body.data).toEqual([]); + }); + }); + + +}); \ No newline at end of file diff --git a/__tests__/walletService.test.js b/__tests__/walletService.test.js new file mode 100644 index 0000000..8e06652 --- /dev/null +++ b/__tests__/walletService.test.js @@ -0,0 +1,132 @@ +import { jest } from '@jest/globals'; + +// Mock the entire service module +jest.mock(process.cwd() + '/src/services/walletService.js', () => { + const mockService = { + createWallet: jest.fn(), + getAllWallets: jest.fn(), + getWalletByCoin: jest.fn() + }; + return { + __esModule: true, + default: mockService + }; +}); + +import walletService from '../src/services/walletService.js'; + +// Ensure the methods are Jest mocks +walletService.createWallet = jest.fn(); +walletService.getAllWallets = jest.fn(); +walletService.getWalletByCoin = jest.fn(); + +describe('WalletService', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createWallet', () => { + it('should successfully create a TRX wallet', async () => { + const mockResponse = { + success: true, + data: { + id: 'wallet_123', + coin: 'trx', + address: 'TXYZ1234567890', + status: 'active' + } + }; + + walletService.createWallet.mockResolvedValue(mockResponse); + + const result = await walletService.createWallet('trx'); + + expect(result.success).toBe(true); + expect(result.data.coin).toBe('trx'); + expect(walletService.createWallet).toHaveBeenCalledWith('trx'); + }); + + it('should handle invalid coin type', async () => { + const mockError = { + success: false, + error: 'Invalid coin type. Must be either "trx" or "bnb"' + }; + + walletService.createWallet.mockResolvedValue(mockError); + + const result = await walletService.createWallet('invalid'); + + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid coin type'); + }); + }); + + describe('getAllWallets', () => { + it('should fetch all wallets successfully', async () => { + const mockResponse = { + success: true, + data: [ + { id: 'wallet_1', coin: 'trx' }, + { id: 'wallet_2', coin: 'bnb' } + ] + }; + + walletService.getAllWallets.mockResolvedValue(mockResponse); + + const result = await walletService.getAllWallets(); + + expect(result.success).toBe(true); + expect(result.data.length).toBe(2); + expect(walletService.getAllWallets).toHaveBeenCalled(); + }); + + it('should handle empty wallet list', async () => { + const mockResponse = { + success: true, + data: [] + }; + + walletService.getAllWallets.mockResolvedValue(mockResponse); + + const result = await walletService.getAllWallets(); + + expect(result.success).toBe(true); + expect(result.data).toEqual([]); + }); + }); + + describe('getWalletByCoin', () => { + it('should fetch BNB wallet successfully', async () => { + const mockResponse = { + success: true, + data: { + id: 'wallet_456', + coin: 'bnb', + address: 'bnb1234567890' + } + }; + + walletService.getWalletByCoin.mockResolvedValue(mockResponse); + + const result = await walletService.getWalletByCoin('bnb'); + + expect(result.success).toBe(true); + expect(result.data.coin).toBe('bnb'); + expect(walletService.getWalletByCoin).toHaveBeenCalledWith('bnb'); + }); + + it('should handle wallet not found', async () => { + const mockError = { + success: false, + error: 'Wallet not found for coin: trx' + }; + + walletService.getWalletByCoin.mockResolvedValue(mockError); + + const result = await walletService.getWalletByCoin('trx'); + + expect(result.success).toBe(false); + expect(result.error).toContain('not found'); + }); + }); +}); \ No newline at end of file diff --git a/src/controllers/wallet.js b/src/controllers/wallet.js deleted file mode 100644 index 6bd15b3..0000000 --- a/src/controllers/wallet.js +++ /dev/null @@ -1,201 +0,0 @@ -import axios from 'axios'; -import dotenv from 'dotenv'; - -dotenv.config(); - -const BITNOB_API_URL = 'https://sandboxapi.bitnob.co/api/v1'; -const BITNOB_API_KEY = process.env.BITNOB_API_KEY; - -// Debug logs for API key -console.log('API Key loaded:', BITNOB_API_KEY ? 'Yes' : 'No'); -console.log('API Key length:', BITNOB_API_KEY ? BITNOB_API_KEY.length : 0); -console.log('API Key format check:', BITNOB_API_KEY ? (BITNOB_API_KEY.startsWith('sk.') ? 'Correct format' : 'Incorrect format') : 'No key'); - -// Create a new wallet -export const createWallet = async (req, res) => { - try { - // Log the incoming request - console.log('Incoming request headers:', req.headers); - console.log('Incoming request body:', req.body); - - if (!req.body) { - return res.status(400).json({ - status: false, - message: 'Request body is required' - }); - } - - const { coin } = req.body; - - if (!coin || !['trx', 'bnb'].includes(coin)) { - return res.status(400).json({ - status: false, - message: 'Invalid coin type. Must be either "trx" or "bnb"' - }); - } - - if (!BITNOB_API_KEY) { - return res.status(500).json({ - status: false, - message: 'API key is not configured' - }); - } - - if (!BITNOB_API_KEY.startsWith('sk.')) { - return res.status(500).json({ - status: false, - message: 'Invalid API key format. Must start with "sk."' - }); - } - - // Debug log to check request details - console.log('Making request to:', `${BITNOB_API_URL}/wallets/create-new-crypto-wallet`); - console.log('Request body:', { coin }); - console.log('Request headers:', { - 'accept': 'application/json', - 'content-type': 'application/json', - 'Authorization': `Bearer ${BITNOB_API_KEY.substring(0, 5)}...` - }); - - const response = await axios({ - method: 'POST', - url: `${BITNOB_API_URL}/wallets/create-new-crypto-wallet`, - headers: { - 'accept': 'application/json', - 'content-type': 'application/json', - 'Authorization': `Bearer ${BITNOB_API_KEY.trim()}` - }, - data: { coin } - }); - - console.log('Bitnob API response:', { - status: response.status, - statusText: response.statusText, - data: response.data - }); - - return res.status(200).json(response.data); - } catch (error) { - console.error('Error details:', { - message: error.message, - status: error.response?.status, - data: error.response?.data, - headers: error.response?.headers, - config: { - url: error.config?.url, - method: error.config?.method, - headers: { - ...error.config?.headers, - Authorization: error.config?.headers?.Authorization ? 'Bearer sk...' : undefined - } - } - }); - - if (error.response) { - // The request was made and the server responded with a status code - return res.status(error.response.status).json({ - status: false, - message: error.response.data?.message || 'API request failed', - details: error.response.data - }); - } else if (error.request) { - // The request was made but no response was received - return res.status(500).json({ - status: false, - message: 'No response received from Bitnob API' - }); - } else { - // Something happened in setting up the request - // return res.status(500).json({ - // status: false, - // message: error.message || 'Error setting up the request' - // }); - } - } -}; - -// Get all wallets -export const getAllWallets = async (req, res) => { - try { - if (!BITNOB_API_KEY) { - return res.status(500).json({ - status: false, - message: 'API key is not configured' - }); - } - - const response = await axios({ - method: 'GET', - url: `${BITNOB_API_URL}/wallets`, - headers: { - 'accept': 'application/json', - 'Authorization': `Bearer ${BITNOB_API_KEY.trim()}` - } - }); - - return res.status(200).json(response.data); - } catch (error) { - console.error('Error fetching wallets:', error.message); - - if (error.response) { - return res.status(error.response.status).json({ - status: false, - message: error.response.data?.message || 'Failed to fetch wallets', - details: error.response.data - }); - } - - return res.status(500).json({ - status: false, - message: 'Error fetching wallets' - }); - } -}; - -// Get wallet by coin -export const getWalletByCoin = async (req, res) => { - try { - const { coin } = req.params; - - if (!coin || !['trx', 'bnb'].includes(coin)) { - return res.status(400).json({ - status: false, - message: 'Invalid coin type. Must be either "trx" or "bnb"' - }); - } - - if (!BITNOB_API_KEY) { - return res.status(500).json({ - status: false, - message: 'API key is not configured' - }); - } - - const response = await axios({ - method: 'GET', - url: `${BITNOB_API_URL}/wallets/crypto-wallet/${coin}`, - headers: { - 'accept': 'application/json', - 'Authorization': `Bearer ${BITNOB_API_KEY.trim()}` - } - }); - - return res.status(200).json(response.data); - } catch (error) { - console.error('Error fetching wallet:', error.message); - - if (error.response) { - return res.status(error.response.status).json({ - status: false, - message: error.response.data?.message || 'Failed to fetch wallet', - details: error.response.data - }); - } - - return res.status(500).json({ - status: false, - message: 'Error fetching wallet' - }); - } -}; - diff --git a/src/controllers/walletController.js b/src/controllers/walletController.js new file mode 100644 index 0000000..1a3ae92 --- /dev/null +++ b/src/controllers/walletController.js @@ -0,0 +1,68 @@ +import { z } from 'zod'; +import walletService from '../services/walletService.js'; + +const walletCreateSchema = z.object({ + coin: z.enum(['trx', 'bnb']) +}); + +export const createWallet = async (req, res) => { + try { + const validationResult = walletCreateSchema.safeParse(req.body); + if (!validationResult.success) { + return res.status(400).json({ + success: false, + error: 'Invalid input', + details: validationResult.error.errors + }); + } + + const { coin } = req.body; + const result = await walletService.createWallet(coin); + + if (!result.success) { + return res.status(400).json(result); + } + + return res.status(201).json(result); + } catch (error) { + return res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}; + +export const getAllWallets = async (req, res) => { + try { + const result = await walletService.getAllWallets(); + + if (!result.success) { + return res.status(400).json(result); + } + + return res.status(200).json(result); + } catch (error) { + return res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}; + +export const getWalletByCoin = async (req, res) => { + try { + const { coin } = req.params; + const result = await walletService.getWalletByCoin(coin); + + if (!result.success) { + return res.status(400).json(result); + } + + return res.status(200).json(result); + } catch (error) { + return res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}; diff --git a/src/index.js b/src/index.js index 969fc3b..06d431d 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,7 @@ import cors from 'cors'; import compression from 'compression'; // Routes -import walletRoute from './routes/walletRoute.js'; +import walletRoute from './routes/walletRoutes.js'; import { customersRouter } from './routes/customersRoutes.js'; import currencyRoutes from './routes/currencyRoutes.js'; import transactionRoutes from './routes/transactionRoutes.js'; diff --git a/src/routes/walletRoute.js b/src/routes/walletRoute.js deleted file mode 100644 index a76aef9..0000000 --- a/src/routes/walletRoute.js +++ /dev/null @@ -1,15 +0,0 @@ -import express from 'express'; -import { createWallet, getAllWallets, getWalletByCoin } from '../controllers/wallet.js'; - -const router = express.Router(); - -// Create wallet -router.post('/create', createWallet); - -// List all wallets -router.get('/', getAllWallets); - -// Get wallet by Coin -router.get('/coin/:coin', getWalletByCoin); - -export default router; \ No newline at end of file diff --git a/src/routes/walletRoutes.js b/src/routes/walletRoutes.js new file mode 100644 index 0000000..2573459 --- /dev/null +++ b/src/routes/walletRoutes.js @@ -0,0 +1,14 @@ +import express from 'express'; +import { + createWallet, + getAllWallets, + getWalletByCoin +} from '../controllers/walletController.js'; + +const router = express.Router(); + +router.post('/', createWallet); +router.get('/', getAllWallets); +router.get('/:coin', getWalletByCoin); + +export default router; \ No newline at end of file diff --git a/src/services/walletService.js b/src/services/walletService.js new file mode 100644 index 0000000..398b1f8 --- /dev/null +++ b/src/services/walletService.js @@ -0,0 +1,81 @@ +import axios from 'axios'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const BITNOB_API_URL = process.env.BITNOB_API_URL || 'https://sandboxapi.bitnob.co/api/v1'; +const BITNOB_API_KEY = process.env.BITNOB_API_KEY; + +class WalletService { + constructor() { + this.client = axios.create({ + baseURL: BITNOB_API_URL, + headers: { + 'Authorization': `Bearer ${BITNOB_API_KEY}`, + 'Content-Type': 'application/json' + } + }); + } + + async createWallet(coin) { + try { + if (!['trx', 'bnb'].includes(coin)) { + return { + success: false, + error: 'Invalid coin type. Must be either "trx" or "bnb"' + }; + } + + const response = await this.client.post('/wallets/create-new-crypto-wallet', { coin }); + + return { + success: true, + data: response.data + }; + } catch (error) { + return { + success: false, + error: error.response?.data?.message || 'Failed to create wallet' + }; + } + } + + async getAllWallets() { + try { + const response = await this.client.get('/wallets'); + return { + success: true, + data: response.data + }; + } catch (error) { + return { + success: false, + error: error.response?.data?.message || 'Failed to fetch wallets' + }; + } + } + + async getWalletByCoin(coin) { + try { + if (!['trx', 'bnb'].includes(coin)) { + return { + success: false, + error: 'Invalid coin type. Must be either "trx" or "bnb"' + }; + } + + const response = await this.client.get(`/wallets/crypto-wallet/${coin}`); + return { + success: true, + data: response.data + }; + } catch (error) { + return { + success: false, + error: error.response?.data?.message || 'Failed to fetch wallet' + }; + } + } +} + +export default new WalletService(); \ No newline at end of file From a74859f1ac7458af269d8ebde78cc6d15014e085 Mon Sep 17 00:00:00 2001 From: Yuyi5la Date: Sun, 1 Jun 2025 18:02:00 +0100 Subject: [PATCH 4/4] fix: log errors in catch blocks to avoid ESLint no-unused-vars warnings --- src/controllers/walletController.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/controllers/walletController.js b/src/controllers/walletController.js index 1a3ae92..3b35f6e 100644 --- a/src/controllers/walletController.js +++ b/src/controllers/walletController.js @@ -24,7 +24,8 @@ export const createWallet = async (req, res) => { } return res.status(201).json(result); - } catch (error) { + } catch (error) { + console.error(error); return res.status(500).json({ success: false, error: 'Internal server error' @@ -41,7 +42,8 @@ export const getAllWallets = async (req, res) => { } return res.status(200).json(result); - } catch (error) { + } catch (error) { + console.error(error); return res.status(500).json({ success: false, error: 'Internal server error' @@ -60,6 +62,7 @@ export const getWalletByCoin = async (req, res) => { return res.status(200).json(result); } catch (error) { + console.error(error); return res.status(500).json({ success: false, error: 'Internal server error'