From 79a36d2b82ecd3fcbbb6cea371d5d6cc0648a69d Mon Sep 17 00:00:00 2001 From: aliMohtarami Date: Wed, 22 Jan 2025 11:07:57 +0330 Subject: [PATCH] payment module refactoring --- src/products/entities/payment.entity.ts | 41 ++++++++++++++++ src/products/products.controller.ts | 65 +++++++++++++++++++++++-- src/products/products.module.ts | 3 +- src/products/products.service.ts | 47 +++++++++++++++++- 4 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 src/products/entities/payment.entity.ts diff --git a/src/products/entities/payment.entity.ts b/src/products/entities/payment.entity.ts new file mode 100644 index 0000000..4535b29 --- /dev/null +++ b/src/products/entities/payment.entity.ts @@ -0,0 +1,41 @@ +import { BelongsTo, Column, DataType, ForeignKey, Model, Table } from "sequelize-typescript"; +import { User } from "src/users/entities/user.entity"; +import { Wallet } from "src/wallet/entities/wallet.entity"; + +@Table +export class Payment extends Model { + @ForeignKey(() => User) + @Column + userId: number; + + @BelongsTo(() => User, { onDelete: "CASCADE" }) + user: User; + + @ForeignKey(() => Wallet) + @Column + walletId: number; + + @BelongsTo(() => Wallet, { onDelete: "CASCADE" }) + wallet: Wallet; + + @Column({ + type: DataType.INTEGER, + allowNull: false, + }) + paymentAmount: number; + + + @Column({ + type: DataType.ENUM( "completed", "failed",), + allowNull: false, + defaultValue: "failed", + }) + status: string; + + @Column({ + type: DataType.DATE, + allowNull: false, + defaultValue: DataType.NOW, + }) + paymentDate: Date; +} diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 654c642..1933f16 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -8,13 +8,19 @@ import { AddToCartDto } from "./dto/cart/add-to-cart.dto"; import { JwtAuthGuard } from "src/guard/auth.guard"; import { UpdateCartDto } from "./dto/cart/update-cart.dto"; import { PaymentService } from "src/payment/payment.service"; +import { InvoiceService } from "src/invoice/invoice.service"; +import { InjectModel } from "@nestjs/sequelize"; +import { Transaction } from "./entities/transaction.entity"; +import { Payment } from "./entities/payment.entity"; @Controller("shop") export class ProductsController { constructor( private readonly productsService: ProductsService, private paymentService: PaymentService, - + private readonly invoiceService: InvoiceService, + @InjectModel(Transaction) private readonly transactionModel: typeof Transaction, + @InjectModel(Payment) private readonly paymentModel: typeof Payment, ) {} ////////////////////////////////////////products//////////////////////////////////////// @@ -111,7 +117,7 @@ export class ProductsController { ////////////////////////////////////////wallet//////////////////////////////////////// //getting wallet balance (user) @UseGuards(JwtAuthGuard) - @Get('wallet') + @Get("wallet") async getBalance(@Request() req) { const userId = req.user.id; return this.productsService.getBalance(userId); @@ -138,6 +144,59 @@ export class ProductsController { async getTransactionByIdForAdmin(@Param("id") id: number) { return this.productsService.getTransactionByIdForAdmin(id); } - + ////////////////////////////////////////payment//////////////////////////////////////// + //payment request + @UseGuards(JwtAuthGuard) + @Get("payment/request") + async requestPayment(@Request() req) { + const userId = req.user.id; + const invoice = await this.invoiceService.getInvoicePendingByUser(userId); + const totalAmount = invoice.totalPaymentAmount; + if (totalAmount < 1000) { + return { message: "please enter amount above 1000" }; + } + const callbackUrl = `http://localhost:3000/payment/verify?userId=${userId}&amount=${totalAmount}`; + const paymentUrl = await this.paymentService.requestPayment(totalAmount, "Purchase products", callbackUrl); + + return { url: paymentUrl }; + } + //payment verify + @Get("verify") + async verifyPayment(@Query() query: { Authority: string; Status: string; userId: number; amount: number }): Promise { + const { Authority, Status, userId, amount } = query; + + if (Status !== "OK") { + throw new Error("Payment failed"); + } + if (!userId) { + throw new Error("User ID is required."); + } + const wallet = this.productsService.getWalletInfo(userId); + try { + const refId = await this.paymentService.verifyPayment(Authority, amount); + await this.productsService.addBalance(userId, amount); + const wallet = this.productsService.getWalletInfo(userId); + await this.paymentModel.create({ + userId, + walletId: (await wallet).walletId, + paymentAmount: amount, + status: "completed", + }); + await this.transactionModel.create({ + walletId: (await wallet).walletId, + amount: String(amount).startsWith("+") ? String(amount) : `+${amount}`, + }); + return { message: "Payment successful", refId }; + } catch (error) { + console.log(error); + await this.paymentModel.create({ + userId, + walletId: (await wallet).walletId, + paymentAmount: amount, + status: "failed", + }); + throw new Error(`Error during payment verification: ${error.message}`); + } + } } diff --git a/src/products/products.module.ts b/src/products/products.module.ts index 9e0d594..0dfda18 100644 --- a/src/products/products.module.ts +++ b/src/products/products.module.ts @@ -13,9 +13,10 @@ import { WalletModule } from "src/wallet/wallet.module"; import { Wallet } from "./entities/wallet.entity"; import { Transaction } from "./entities/transaction.entity"; import { PaymentService } from "src/payment/payment.service"; +import { Payment } from "./entities/payment.entity"; @Module({ - imports: [SequelizeModule.forFeature([Product,Cart,Invoice,Wallet, Transaction]), + imports: [SequelizeModule.forFeature([Product,Cart,Invoice,Wallet, Transaction,Payment]), JwtModule.register({ secret: process.env.JWT_SECRET, signOptions: { expiresIn: '1h' }, diff --git a/src/products/products.service.ts b/src/products/products.service.ts index 68558f0..384b937 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -11,9 +11,12 @@ import { InvoiceService } from "src/invoice/invoice.service"; import { WalletService } from "src/wallet/WalletService"; import { Wallet } from "./entities/wallet.entity"; import { Transaction } from "./entities/transaction.entity"; +import { InternalServerErrorException } from "@nestjs/common"; +const ZarinpalCheckout = require("zarinpal-checkout"); @Injectable() export class ProductsService { + private zarinpal; constructor( @InjectModel(Product) private readonly productModel: typeof Product, @InjectModel(Cart) private readonly cartModel: typeof Cart, @@ -22,7 +25,14 @@ export class ProductsService { @InjectModel(Transaction) private transactionModel: typeof Transaction, private invoiceService: InvoiceService, private walletService: WalletService, - ) {} + ) { + 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); + } ///////////////////////////////////////////products////////////////////////////////////////////// // create a new product async create(createProductDto: CreateProductDto): Promise { @@ -437,4 +447,39 @@ export class ProductsService { where: { walletId: wallet.walletId }, }); } + //charge balance of wallet by user + async addBalance(userId: number, amount: number) { + try { + const wallet = await this.walletModel.findOne({ where: { userId } }); + if (wallet) { + wallet.balance += Number(amount); + await wallet.save(); + return { message: "Balance updated successfully.", balance: wallet.balance }; + } else { + const newWallet = await this.walletModel.create({ userId, balance: amount }); + return { message: "Wallet created and balance added successfully.", balance: newWallet.balance }; + } + } catch (error) { + throw new HttpException("An error occurred while adding balance to the wallet.", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + ///////////////////////////////////////////payment////////////////////////////////////////////// + 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.message || error); + throw new InternalServerErrorException(`Error in payment request: ${error.message}`); + } + } }