diff --git a/migrations/20250105085732-create-invoice.js b/migrations/20250105085732-create-invoice.js new file mode 100644 index 0000000..2cf1a2f --- /dev/null +++ b/migrations/20250105085732-create-invoice.js @@ -0,0 +1,53 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Invoices', { + id: { + type: Sequelize.INTEGER, + allowNull: false, + autoIncrement: true, + primaryKey: true, + }, + userId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'Users', + key: 'id', + }, + onDelete: 'CASCADE', + }, + totalAmount: { + type: Sequelize.INTEGER, + allowNull: false, + }, + products: { + type: Sequelize.JSON, + allowNull: false, + }, + paymentStatus: { + type: Sequelize.STRING, + allowNull: false, + }, + refId: { + type: Sequelize.STRING, + allowNull: false, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.NOW, + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.NOW, + }, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Invoices'); + }, +}; diff --git a/src/cart/cart.controller.ts b/src/cart/cart.controller.ts index 44a320f..9f43d33 100644 --- a/src/cart/cart.controller.ts +++ b/src/cart/cart.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Patch, Delete, Body, Param, UseGuards, Request } from "@nestjs/common"; +import { Controller, Get, Post, Patch, Delete, Body, Param, UseGuards, Request, HttpException, HttpStatus } from "@nestjs/common"; import { CartService } from "./cart.service"; import { JwtAuthGuard } from "src/guard/auth.guard"; import { AddToCartDto } from "./dto/add-to-cart.dto"; @@ -43,4 +43,21 @@ export class CartController { message: "Product removed from cart successfully", }; } + + @Post(':userId/checkout') + async processOrder( + @Param('userId') userId: number, + @Body('totalAmount') totalAmount: number, + ): Promise { + if (!totalAmount || totalAmount <= 0) { + throw new HttpException('Invalid total amount.', HttpStatus.BAD_REQUEST); + } + + try { + const result = await this.cartService.processOrder(userId, totalAmount); + return result; + } catch (error) { + throw new HttpException(error.message || 'Order processing failed.', HttpStatus.INTERNAL_SERVER_ERROR); + } + } } diff --git a/src/cart/cart.module.ts b/src/cart/cart.module.ts index bbca002..44e5a85 100644 --- a/src/cart/cart.module.ts +++ b/src/cart/cart.module.ts @@ -1,4 +1,4 @@ -import { Module } from "@nestjs/common"; +import { Module, forwardRef } from "@nestjs/common"; import { CartService } from "./cart.service"; import { CartController } from "./cart.controller"; import { Cart } from "./entities/cart.entity"; @@ -7,15 +7,21 @@ import { User } from "src/users/entities/user.entity"; import { Product } from "src/products/entities/product.entity"; import { JwtModule } from "@nestjs/jwt"; import { JwtAuthGuard } from "src/guard/auth.guard"; +import { WalletModule } from "src/wallet/wallet.module"; +import { InvoiceModule } from "src/invoice/invoice.module"; @Module({ - imports: [SequelizeModule.forFeature([Cart,User,Product]), - JwtModule.register({ - secret: process.env.JWT_SECRET, - signOptions: { expiresIn: '1h' }, - }) -], + imports: [ + SequelizeModule.forFeature([Cart, User, Product]), + JwtModule.register({ + secret: process.env.JWT_SECRET, + signOptions: { expiresIn: "1h" }, + }), + WalletModule, + forwardRef(()=>InvoiceModule), + ], controllers: [CartController], - providers: [CartService,JwtAuthGuard], + providers: [CartService, JwtAuthGuard], + exports: [CartService], }) export class CartModule {} diff --git a/src/cart/cart.service.ts b/src/cart/cart.service.ts index 8fab957..dd0948c 100644 --- a/src/cart/cart.service.ts +++ b/src/cart/cart.service.ts @@ -1,18 +1,20 @@ -import { Injectable, HttpException, HttpStatus } from "@nestjs/common"; +import { Injectable, HttpException, HttpStatus, Inject, forwardRef } from "@nestjs/common"; import { InjectModel } from "@nestjs/sequelize"; import { Cart } from "./entities/cart.entity"; -import { CartResponse } from "./cart.response"; -import { User } from "src/users/entities/user.entity"; import { Product } from "src/products/entities/product.entity"; -import { console } from "inspector"; +import { WalletService } from "src/wallet/wallet.service"; +import { InvoiceService } from "src/invoice/invoice.service"; @Injectable() export class CartService { constructor( @InjectModel(Cart) private readonly cartModel: typeof Cart, @InjectModel(Product) private readonly productModel: typeof Product, + private readonly walletService: WalletService, + @Inject(forwardRef(() => InvoiceService)) + private invoiceService: InvoiceService ) {} - + // Add product to cart async addToCart(addToCartDto: { userId: number; productId: number; quantity: number }): Promise<{ message: string; cartItem: Cart }> { const { userId, productId, quantity } = addToCartDto; @@ -51,6 +53,10 @@ export class CartService { // Get user's cart async getUserCart(userId: number): Promise<{ cartItems: Cart[]; totalPrice: number }> { + if (!userId) { + throw new HttpException("User ID is required.", HttpStatus.BAD_REQUEST); + } + const cartItems = await this.cartModel.findAll({ where: { userId }, include: [ @@ -65,7 +71,7 @@ export class CartService { throw new HttpException("No cart items found for the specified user.", HttpStatus.NOT_FOUND); } - const totalPrice = cartItems.reduce((sum, item) => sum + Number(item.totalPrice), 0); + const totalPrice = cartItems.reduce((sum, item) => sum + (Number(item.totalPrice) || 0), 0); return { cartItems, totalPrice }; } @@ -84,7 +90,7 @@ export class CartService { return cartItem; } - // Remove product from cart + // Remove an item from cart async removeFromCart(userId: number, productId: number): Promise<{ message: string }> { const cartItem = await this.cartModel.findOne({ where: { userId, productId } }); @@ -95,4 +101,40 @@ export class CartService { await cartItem.destroy(); return { message: "Item deleted from your cart successfully." }; } + + //delete whole cart + async clearCart(userId: number): Promise { + await this.cartModel.destroy({ where: { userId } }); + } + + //order(clearCart unable) + async processOrder(userId: number, totalAmount: number): Promise { + // Deducting credit from wallet + await this.walletService.processPayment(userId, totalAmount); + //Reduce the number purchased from the number of products + const cartItems = await this.cartModel.findAll({ where: { userId } }); + if (cartItems.length === 0) { + throw new HttpException("Cart is empty.", HttpStatus.BAD_REQUEST); + } + for (const cartItem of cartItems) { + const { productId, quantity } = cartItem; + + const product = await this.productModel.findOne({ where: { id: productId } }); + + if (!product) { + throw new HttpException(`Product with ID ${productId} not found.`, HttpStatus.NOT_FOUND); + } + + if (product.quantity < quantity) { + throw new HttpException(`Insufficient stock for product ID ${productId}.`, HttpStatus.BAD_REQUEST); + } + + product.quantity -= quantity; + await product.save(); + // await this.clearCart(userId); + } + const invoice = await this.invoiceService.createInvoiceFromCart(userId) + + return "Order processed successfully"; + } } diff --git a/src/invoice/entities/invoice.entity.ts b/src/invoice/entities/invoice.entity.ts index f90ba5f..6e8d398 100644 --- a/src/invoice/entities/invoice.entity.ts +++ b/src/invoice/entities/invoice.entity.ts @@ -1,6 +1,5 @@ -import { Table, Model, Column, BelongsTo, ForeignKey } from "sequelize-typescript"; +import { Table, Column, ForeignKey, BelongsTo, DataType, Model } from "sequelize-typescript"; import { User } from "../../users/entities/user.entity"; -import { Product } from "../../products/entities/product.entity"; @Table export class Invoice extends Model { @@ -8,9 +7,23 @@ export class Invoice extends Model { @Column userId: number; - @BelongsTo(() => User, { onDelete: 'CASCADE' }) + @BelongsTo(() => User, { onDelete: "CASCADE" }) user: User; @Column totalAmount: number; + + @Column({ type: DataType.JSON }) + products: { + productId: number; + quantity: number; + price: number; + name: string; + }[]; + + @Column + paymentStatus: string; + + @Column + refId: string; } diff --git a/src/invoice/invoice.controller.ts b/src/invoice/invoice.controller.ts index 5a3bf6b..96b7a9c 100644 --- a/src/invoice/invoice.controller.ts +++ b/src/invoice/invoice.controller.ts @@ -1,14 +1,13 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from "@nestjs/common"; import { InvoiceService } from "./invoice.service"; -import { Invoice } from "./entities/invoice.entity"; @Controller("invoice") export class InvoiceController { constructor(private readonly invoiceService: InvoiceService) {} @Post("create") - async createInvoice(@Body() body: { userId: number; totalAmount: number }): Promise { - const { userId, totalAmount } = body; - return this.invoiceService.createInvoice(userId, totalAmount); + async createInvoice(@Body() body: { userId: number; totalAmount: number; products: any[]; refId: string; paymentStatus: string }) { + const { userId, totalAmount, products, refId, paymentStatus } = body; + return this.invoiceService.createInvoice(userId, totalAmount, products, refId, paymentStatus); } @Get(":userId") async getInvoices(@Param("userId") userId: number): Promise { diff --git a/src/invoice/invoice.module.ts b/src/invoice/invoice.module.ts index 3199646..69adf63 100644 --- a/src/invoice/invoice.module.ts +++ b/src/invoice/invoice.module.ts @@ -1,12 +1,14 @@ -import { Module } from '@nestjs/common'; -import { InvoiceService } from './invoice.service'; -import { InvoiceController } from './invoice.controller'; -import { SequelizeModule } from '@nestjs/sequelize'; -import { Invoice } from './entities/invoice.entity'; +import { Module, forwardRef } from "@nestjs/common"; +import { SequelizeModule } from "@nestjs/sequelize"; +import { InvoiceController } from "./invoice.controller"; +import { InvoiceService } from "./invoice.service"; +import { Invoice } from "./entities/invoice.entity"; +import { CartModule } from "src/cart/cart.module"; @Module({ - imports : [SequelizeModule.forFeature([Invoice])], + imports: [SequelizeModule.forFeature([Invoice]), forwardRef(()=>CartModule)], controllers: [InvoiceController], providers: [InvoiceService], + exports: [InvoiceService], }) export class InvoiceModule {} diff --git a/src/invoice/invoice.service.ts b/src/invoice/invoice.service.ts index e871aab..23b43b7 100644 --- a/src/invoice/invoice.service.ts +++ b/src/invoice/invoice.service.ts @@ -1,33 +1,62 @@ -import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; +import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from "@nestjs/common"; import { InjectModel } from "@nestjs/sequelize"; import { Invoice } from "./entities/invoice.entity"; -import { where } from "sequelize"; +import { CartService } from "src/cart/cart.service"; +import { User } from "src/users/entities/user.entity"; @Injectable() export class InvoiceService { - constructor(@InjectModel(Invoice) private readonly invoiceModel: typeof Invoice) {} + constructor( + @InjectModel(Invoice) private readonly invoiceModel: typeof Invoice, + @Inject(forwardRef(() => CartService)) + private cartService: CartService, + ) {} - async createInvoice(userId: number, totalAmount: number): Promise { - try { - if (!userId) { - throw new HttpException("User id not found!", HttpStatus.BAD_REQUEST); - } - - const newInvoice = await this.invoiceModel.create({ userId, totalAmount }); - return newInvoice; - } catch (error) { - - if (error instanceof HttpException) { - throw error; - } + async createInvoiceFromCart(userId: number): Promise { + const user = await User.findByPk(userId); + if (!user) { + throw new HttpException("User not found", HttpStatus.NOT_FOUND); + } - throw new HttpException("An error occurred while creating the invoice.", HttpStatus.INTERNAL_SERVER_ERROR); + const userCartItems = await this.cartService.getUserCart(userId); + if (!userCartItems || userCartItems.cartItems.length === 0) { + throw new HttpException("Cart is empty", HttpStatus.BAD_REQUEST); } + + const totalAmount = userCartItems.totalPrice; + + const products = userCartItems.cartItems.map(item => ({ + productId: item.productId, + quantity: item.quantity, + price: item.productPrice, + name: item.productName, + })); + + const newInvoice = await this.invoiceModel.create({ + userId, + totalAmount, + products, + paymentStatus: "pending", + refId: "", + }); + + return newInvoice; + } + async createInvoice(userId: number, totalAmount: number, products: any[], refId: string, paymentStatus: string): Promise { + const newInvoice = await this.invoiceModel.create({ + userId, + totalAmount, + products, + refId, + paymentStatus, + }); + + return newInvoice; } async getInvoicesByUser(userId: number): Promise { try { if (!userId) { - throw new HttpException('User ID is required.', HttpStatus.BAD_REQUEST); + throw new HttpException("User ID is required.", HttpStatus.BAD_REQUEST); } const invoices = await this.invoiceModel.findAll({ @@ -35,7 +64,7 @@ export class InvoiceService { }); if (!invoices || invoices.length === 0) { - throw new HttpException('No invoices found for this user.', HttpStatus.NOT_FOUND); + throw new HttpException("No invoices found for this user.", HttpStatus.NOT_FOUND); } return invoices; @@ -43,10 +72,7 @@ export class InvoiceService { if (error instanceof HttpException) { throw error; } - throw new HttpException( - 'An error occurred while retrieving invoices.', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw new HttpException("An error occurred while retrieving invoices.", HttpStatus.INTERNAL_SERVER_ERROR); } } } diff --git a/src/payment/payment.controller.ts b/src/payment/payment.controller.ts index 330d5f3..dfa41b3 100644 --- a/src/payment/payment.controller.ts +++ b/src/payment/payment.controller.ts @@ -1,25 +1,59 @@ -import { Controller, Get, Query } from '@nestjs/common'; -import { PaymentService } from './payment.service'; +import { Controller, Post, Body, Param, Get, Query } from "@nestjs/common"; +import { PaymentService } from "./payment.service"; +import { CartService } from "../cart/cart.service"; +import { InvoiceService } from "../invoice/invoice.service"; +import { WalletService } from "src/wallet/wallet.service"; -@Controller('payment') +@Controller("payment") export class PaymentController { - constructor(private readonly paymentService: PaymentService) {} - @Get('request') - async requestPayment(){ - const amount = 10000; - const description = 'Test payment'; - const callbackUrl = 'http://localhost:3000/payment/verify'; - const paymentUrl = await this.paymentService.requestPayment(amount, description, callbackUrl); - return { paymentUrl }; + constructor( + private readonly wallet: WalletService, + private readonly paymentService: PaymentService, + private readonly cartService: CartService, + ) {} + + @Post("request/:userId") + async requestPayment(@Param("userId") userId: number): Promise<{ url: string }> { + const userCartItems = await this.cartService.getUserCart(userId); + + if (!userCartItems || userCartItems.cartItems.length === 0) { + throw new Error("Cart is empty!"); + } + + const totalAmount = userCartItems.totalPrice; + + const callbackUrl = `http://localhost:3000/payment/verify?userId=${userId}`; + const paymentUrl = await this.paymentService.requestPayment(totalAmount, "Purchase products", callbackUrl); + + return { url: paymentUrl }; } - @Get('verify') - async verifyPayment(@Query('Authority') authority: string, @Query('Status') status: string) { - if (status === 'OK') { - const amount = 10000; - const refId = await this.paymentService.verifyPayment(authority, amount); - return { success: true, refId }; - } else { - return { success: false, message: 'Payment canceled' }; + + @Get("verify") + async verifyPayment(@Query() query: { Authority: string; Status: string; userId: number }): Promise { + const { Authority, Status, userId } = query; + + if (Status !== "OK") { + throw new Error("Payment failed"); + } + + if (!userId) { + throw new Error("User ID is required."); + } + + try { + const userCartItems = await this.cartService.getUserCart(userId); + if (!userCartItems || userCartItems.cartItems.length === 0) { + throw new Error("Cart is empty!"); + } + const totalAmount = userCartItems.totalPrice; + + const refId = await this.paymentService.verifyPayment(Authority, totalAmount); + await this.wallet.addBalance(userId,totalAmount) + return { message: "Payment successful", refId}; + } catch (error) { + console.log(error) + throw new Error(`Error during payment verification: ${error.message}`); } } } + diff --git a/src/payment/payment.module.ts b/src/payment/payment.module.ts index 572f994..1405a69 100644 --- a/src/payment/payment.module.ts +++ b/src/payment/payment.module.ts @@ -1,8 +1,12 @@ import { Module } from '@nestjs/common'; import { PaymentService } from './payment.service'; import { PaymentController } from './payment.controller'; +import { InvoiceService } from 'src/invoice/invoice.service'; +import { CartModule } from 'src/cart/cart.module'; +import { WalletModule } from 'src/wallet/wallet.module'; @Module({ + imports:[CartModule,WalletModule], controllers: [PaymentController], providers: [PaymentService], }) diff --git a/src/payment/payment.service.ts b/src/payment/payment.service.ts index 32266d6..a910e93 100644 --- a/src/payment/payment.service.ts +++ b/src/payment/payment.service.ts @@ -1,37 +1,55 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; + const ZarinpalCheckout = require('zarinpal-checkout'); @Injectable() export class PaymentService { private zarinpal; - constructor() { - this.zarinpal = ZarinpalCheckout.create('00000000-0000-0000-0000-000000000000', true); + constructor( + ) { + this.zarinpal = this.initializeZarinpal(); + } + + private initializeZarinpal() { + const merchantId = '00000000-0000-0000-0000-000000000000'; // Merchant ID should be valid + const sandboxMode = true; + return ZarinpalCheckout.create(merchantId, sandboxMode); } - async requestPayment(amount: number, description: string, callbackUrl: string) { - const result = await this.zarinpal.PaymentRequest({ - Amount: amount, - CallbackURL: callbackUrl, - Description: description, - }); - - if (result.status === 100) { - return result.url; - } else { - throw new Error(`Error in payment request: ${result.status}`); + async requestPayment(amount: number, description: string, callbackUrl: string): Promise { + try { + const result = await this.zarinpal.PaymentRequest({ + Amount: amount, + CallbackURL: callbackUrl, + Description: description, + }); + + if (result.status === 100) { + return result.url; + } else { + throw new Error(`Payment request failed with status: ${result.status}`); + } + } catch (error) { + console.log('Error in PaymentRequest:', error); + throw new InternalServerErrorException(`Error in payment request: ${error.message}`); } } - async verifyPayment(authority: string, amount: number) { - const result = await this.zarinpal.PaymentVerification({ - Amount: amount, - Authority: authority, - }); - - if (result.status === 100) { - return result.RefID; - } else { - throw new Error(`Payment verification failed: ${result.status}`); + + async verifyPayment(authority: string, amount: number): Promise { + try { + const result = await this.zarinpal.PaymentVerification({ + Amount: amount, + Authority: authority, + }); + + if (result.status === 100) { + return result.RefID; + } else { + throw new Error(`Payment verification failed with status: ${result.status}`); + } + } catch (error) { + throw new InternalServerErrorException(`Error in payment verification: ${error.message}`); } } } diff --git a/src/wallet/wallet.controller.ts b/src/wallet/wallet.controller.ts index 18dc9b8..99e002b 100644 --- a/src/wallet/wallet.controller.ts +++ b/src/wallet/wallet.controller.ts @@ -1,19 +1,16 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; -import { WalletService } from './wallet.service'; -import { AddBalanceResponse } from './add-balance-response.interface'; +import { Controller, Get, Post, Body, Patch, Param, Delete } from "@nestjs/common"; +import { WalletService } from "./wallet.service"; +import { AddBalanceResponse } from "./add-balance-response.interface"; -@Controller('wallet') +@Controller("wallet") export class WalletController { constructor(private readonly walletService: WalletService) {} - @Get(':userId') - async getBalance(@Param('userId') userId: number): Promise { + @Get(":userId") + async getBalance(@Param("userId") userId: number): Promise { return this.walletService.getBalance(userId); } - @Post(':userId/add') - async addBalance( - @Param('userId') userId: number, - @Body('amount') amount: number - ): Promise { + @Post(":userId/add") + async addBalance(@Param("userId") userId: number, @Body("amount") amount: number): Promise { return this.walletService.addBalance(userId, amount); } } diff --git a/src/wallet/wallet.module.ts b/src/wallet/wallet.module.ts index d809bb0..62c6385 100644 --- a/src/wallet/wallet.module.ts +++ b/src/wallet/wallet.module.ts @@ -8,5 +8,6 @@ import { SequelizeModule } from '@nestjs/sequelize'; imports: [SequelizeModule.forFeature([Wallet])], controllers: [WalletController], providers: [WalletService], + exports: [WalletService], }) export class WalletModule {} diff --git a/src/wallet/wallet.service.ts b/src/wallet/wallet.service.ts index 2656832..7e0b0cc 100644 --- a/src/wallet/wallet.service.ts +++ b/src/wallet/wallet.service.ts @@ -28,4 +28,20 @@ export class WalletService { throw new HttpException("An error occurred while adding balance to the wallet.", HttpStatus.INTERNAL_SERVER_ERROR); } } + async processPayment(userId: number, amount: number): Promise { + const wallet = await this.walletModel.findOne({ where: { userId } }); + + if (!wallet) { + throw new Error("Wallet not found"); + } + + if (wallet.balance < amount) { + throw new Error("Insufficient funds"); + } + + wallet.balance -= amount; + await wallet.save(); + + return "Payment processed successfully"; + } }