From 87f16e1918769328c358c739edfe74448a042e5e Mon Sep 17 00:00:00 2001 From: maxCastro1 Date: Tue, 28 May 2024 18:14:46 +0200 Subject: [PATCH] implementing stripe payment system --- .github/workflows/ci.yml | 2 + package.json | 1 + src/__test__/getProduct.test.ts | 40 ++++++++++++-- src/controllers/productController.ts | 6 ++- src/entities/User.ts | 3 -- src/entities/transaction.ts | 10 ---- src/routes/ProductRoutes.ts | 5 +- src/services/index.ts | 1 + src/services/orderServices/createOrder.ts | 10 ---- .../getOrderTransactionHistory.ts | 2 - src/services/productServices/payment.ts | 52 +++++++++++++++++++ 11 files changed, 101 insertions(+), 31 deletions(-) create mode 100644 src/services/productServices/payment.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 224592b..3170cd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ env: GOOGLE_CLIENT_ID: ${{secrets.GOOGLE_CLIENT_ID}} GOOGLE_CLIENT_SECRET: ${{secrets.GOOGLE_CLIENT_SECRET}} + STRIPE_SECRET_KEY: ${{secrets.STRIPE_SECRET_KEYT}} + jobs: build-lint-test-coverage: runs-on: ubuntu-latest diff --git a/package.json b/package.json index ef7acc5..53cc5a7 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "reflect-metadata": "^0.2.2", "socket.io": "^4.7.5", "source-map-support": "^0.5.21", + "stripe": "^15.8.0", "superagent": "^9.0.1", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", diff --git a/src/__test__/getProduct.test.ts b/src/__test__/getProduct.test.ts index ecd2281..96201dd 100644 --- a/src/__test__/getProduct.test.ts +++ b/src/__test__/getProduct.test.ts @@ -7,9 +7,11 @@ import { User, UserInterface } from '../entities/User'; import { v4 as uuid } from 'uuid'; import { Product } from '../entities/Product'; import { Category } from '../entities/Category'; +import { Cart } from '../entities/Cart'; import { cleanDatabase } from './test-assets/DatabaseCleanup'; const vendor1Id = uuid(); +const BuyerID = uuid(); const product1Id = uuid(); const Invalidproduct = '11278df2-d026-457a-9471-4749f038df68'; const catId = uuid(); @@ -37,6 +39,18 @@ const sampleVendor1: UserInterface = { photoUrl: 'https://example.com/photo.jpg', role: 'VENDOR', }; +const sampleBuyer1: UserInterface = { + id: BuyerID, + firstName: 'vendor1o', + lastName: 'user', + email: 'buyer10@example.com', + password: 'password', + userType: 'Vendor', + gender: 'Male', + phoneNumber: '000380996348', + photoUrl: 'https://example.com/photo.jpg', + role: 'BUYER', +}; const sampleCat = { id: catId, @@ -53,7 +67,7 @@ const sampleProduct1 = { vendor: sampleVendor1, categories: [sampleCat], }; - +let cardID : string; beforeAll(async () => { const connection = await dbConnection(); @@ -61,7 +75,8 @@ beforeAll(async () => { await categoryRepository?.save({ ...sampleCat }); const userRepository = connection?.getRepository(User); - await userRepository?.save({ ...sampleVendor1 }); + await userRepository?.save({ ...sampleVendor1}); + await userRepository?.save({ ...sampleBuyer1 }); const productRepository = connection?.getRepository(Product); await productRepository?.save({ ...sampleProduct1 }); @@ -69,7 +84,6 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase(); - server.close(); }); @@ -122,3 +136,23 @@ describe('Get single product', () => { expect(response.body.message).toBe('Product not found'); }, 10000); }); +describe('Cart Order and payment functionalities', () => { + it('should create a cart for a product', async () => { + const productId = product1Id; + const quantity = 8; + + const token = getAccessToken(BuyerID, sampleBuyer1.email); + + const response = await request(app) + .post('/cart') + .set('Authorization', `Bearer ${token}`) + .send({ productId, quantity }); + + + expect(response.status).toBe(201); + expect(response.body.data.cart).toBeDefined(); + cardID = JSON.stringify(response.body.data.cart.id) + }); + +} +) \ No newline at end of file diff --git a/src/controllers/productController.ts b/src/controllers/productController.ts index 1cd895a..05aa5a3 100644 --- a/src/controllers/productController.ts +++ b/src/controllers/productController.ts @@ -10,7 +10,8 @@ import { productStatusServices, viewSingleProduct, searchProductService, - listAllProductsService, + listAllProductsService, + confirmPayment, } from '../services'; export const readProduct = async (req: Request, res: Response) => { @@ -70,3 +71,6 @@ export const searchProduct = async (req: Request, res: Response) => { res.status(500).json({ error: 'Internal Server Error' }); } }; +export const Payment = async (req: Request, res: Response) => { + await confirmPayment(req, res); +}; diff --git a/src/entities/User.ts b/src/entities/User.ts index eebd104..b3019ea 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -108,9 +108,6 @@ export class User { @UpdateDateColumn() updatedAt!: Date; - @Column({ type: 'numeric', precision: 24, scale: 2, default: 0 }) - accountBalance!: number; - @BeforeInsert() setRole (): void { this.role = this.userType === 'Vendor' ? roles.vendor : roles.buyer; diff --git a/src/entities/transaction.ts b/src/entities/transaction.ts index 0f7b0ea..defda7f 100644 --- a/src/entities/transaction.ts +++ b/src/entities/transaction.ts @@ -35,16 +35,6 @@ export class Transaction { @IsNumber() amount!: number; - @Column({ type: 'numeric', precision: 15, scale: 2, default: 0 }) - @IsNotEmpty() - @IsNumber() - previousBalance!: number; - - @Column({ type: 'numeric', precision: 15, scale: 2, default: 0 }) - @IsNotEmpty() - @IsNumber() - currentBalance!: number; - @Column({ type: 'enum', enum: ['debit', 'credit'] }) @IsNotEmpty() @IsString() diff --git a/src/routes/ProductRoutes.ts b/src/routes/ProductRoutes.ts index 614eaaf..4a72b47 100644 --- a/src/routes/ProductRoutes.ts +++ b/src/routes/ProductRoutes.ts @@ -1,6 +1,6 @@ import { RequestHandler, Router } from 'express'; -import { productStatus, searchProduct } from '../controllers/index'; +import { productStatus, searchProduct, } from '../controllers/index'; import { hasRole } from '../middlewares/roleCheck'; import upload from '../middlewares/multer'; import { authMiddleware } from '../middlewares/verifyToken'; @@ -18,7 +18,7 @@ import { createOrder, getOrders, updateOrder, - getOrdersHistory, + getOrdersHistory,Payment, getSingleVendorOrder, getVendorOrders, updateVendorOrder, @@ -54,5 +54,6 @@ router.put('/vendor/orders/:id', authMiddleware as RequestHandler, hasRole('VEND router.get('/admin/orders', authMiddleware as RequestHandler, hasRole('ADMIN'), getBuyerVendorOrders); router.get('/admin/orders/:id', authMiddleware as RequestHandler, hasRole('ADMIN'), getSingleBuyerVendorOrder); router.put('/admin/orders/:id', authMiddleware as RequestHandler, hasRole('ADMIN'), updateBuyerVendorOrder); +router.post('/payment/:id', authMiddleware as RequestHandler, hasRole('BUYER'), Payment) export default router; diff --git a/src/services/index.ts b/src/services/index.ts index 12d0aa7..08bdbe4 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -21,6 +21,7 @@ export * from './productServices/listAllProductsService'; export * from './productServices/productStatus'; export * from './productServices/viewSingleProduct'; export * from './productServices/searchProduct'; +export * from './productServices/payment' // Buyer wishlist services export * from './wishListServices/addProduct'; diff --git a/src/services/orderServices/createOrder.ts b/src/services/orderServices/createOrder.ts index 30cbb4a..115646a 100644 --- a/src/services/orderServices/createOrder.ts +++ b/src/services/orderServices/createOrder.ts @@ -60,14 +60,6 @@ export const createOrderService = async (req: Request, res: Response) => { orderItems.push(orderItem); } - if (!buyer.accountBalance || buyer.accountBalance < totalPrice) { - return sendErrorResponse(res, 400, 'Not enough funds to perform this transaction'); - } - - const previousBalance = buyer.accountBalance; - buyer.accountBalance -= totalPrice; - const currentBalance = buyer.accountBalance; - const newOrder = new Order(); newOrder.buyer = buyer; newOrder.totalPrice = totalPrice; @@ -94,8 +86,6 @@ export const createOrderService = async (req: Request, res: Response) => { orderTransaction.user = buyer; orderTransaction.order = newOrder; orderTransaction.amount = totalPrice; - orderTransaction.previousBalance = previousBalance; - orderTransaction.currentBalance = currentBalance; orderTransaction.type = 'debit'; orderTransaction.description = 'Purchase of products'; await transactionalEntityManager.save(Transaction, orderTransaction); diff --git a/src/services/orderServices/getOrderTransactionHistory.ts b/src/services/orderServices/getOrderTransactionHistory.ts index 74ae473..6bd0b17 100644 --- a/src/services/orderServices/getOrderTransactionHistory.ts +++ b/src/services/orderServices/getOrderTransactionHistory.ts @@ -23,8 +23,6 @@ export const getTransactionHistoryService = async (req: Request, res: Response) id: transaction.id, amount: transaction.amount, type: transaction.type, - previousBalance: transaction.previousBalance, - currentBalance: transaction.currentBalance, description: transaction.description, createdAt: transaction.createdAt, order: transaction.order diff --git a/src/services/productServices/payment.ts b/src/services/productServices/payment.ts new file mode 100644 index 0000000..b613296 --- /dev/null +++ b/src/services/productServices/payment.ts @@ -0,0 +1,52 @@ +import { Request, Response } from 'express'; +import { Cart } from '../../entities/Cart'; // Import your Cart entity +import { Order } from '../../entities/Order'; // Import your Order entity +import { getRepository, getTreeRepository } from 'typeorm'; +import dotenv from 'dotenv'; +import Stripe from 'stripe'; +dotenv.config(); +const stripeInstance = new Stripe(process.env.STRIPE_SECRET_KEY as string, { + apiVersion: "2024-04-10", +}); + +export const confirmPayment = async (req: Request, res: Response) => { + try { + const { payment_method } = req.body; + const cartId = req.params.cartId; // Get the cart ID from the params + + const cartRepository = getRepository(Cart); + const orderRepository = getTreeRepository(Order) + const cart = await cartRepository.findOne({where: {id : cartId}}); + if (!cart) { + return res.status(404).json({ error: 'Cart not found.' }); + } + const order = await orderRepository.findOne({ where: { buyer: cart.user } }); + if (!order) { + return res.status(404).json({ error: 'order not found.' }); + } + + const paymentIntent = await stripeInstance.paymentIntents.create({ + amount: cart.totalAmount, // Convert total to cents + currency: 'usd', + description: `Order #${cartId}`, + return_url: 'https://frontend-website.com/success', + confirm: true, + payment_method, + }); + + order.orderStatus = 'awaiting shipment'; + await orderRepository.save(order); + + + if (paymentIntent.status === 'succeeded') { + // Payment succeeded + res.status(200).json({ message: 'Payment successful!' }); + } else { + // Payment failed + res.status(400).json({ error: 'Payment failed.' }); + } + } catch (error) { + console.error('Error confirming payment:', error); + res.status(500).json({ error: 'Something went wrong' }); + } +}; \ No newline at end of file