Establish integration between payment, wallet, cart, and invoice modules

master
nicekid1 2 months ago
parent 9e665f8710
commit 35521f8e0c
  1. 53
      migrations/20250105085732-create-invoice.js
  2. 19
      src/cart/cart.controller.ts
  3. 14
      src/cart/cart.module.ts
  4. 54
      src/cart/cart.service.ts
  5. 19
      src/invoice/entities/invoice.entity.ts
  6. 7
      src/invoice/invoice.controller.ts
  7. 14
      src/invoice/invoice.module.ts
  8. 66
      src/invoice/invoice.service.ts
  9. 74
      src/payment/payment.controller.ts
  10. 4
      src/payment/payment.module.ts
  11. 32
      src/payment/payment.service.ts
  12. 19
      src/wallet/wallet.controller.ts
  13. 1
      src/wallet/wallet.module.ts
  14. 16
      src/wallet/wallet.service.ts

@ -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');
},
};

@ -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<string> {
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);
}
}
}

@ -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]),
imports: [
SequelizeModule.forFeature([Cart, User, Product]),
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' },
})
signOptions: { expiresIn: "1h" },
}),
WalletModule,
forwardRef(()=>InvoiceModule),
],
controllers: [CartController],
providers: [CartService, JwtAuthGuard],
exports: [CartService],
})
export class CartModule {}

@ -1,16 +1,18 @@
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
@ -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<void> {
await this.cartModel.destroy({ where: { userId } });
}
//order(clearCart unable)
async processOrder(userId: number, totalAmount: number): Promise<string> {
// 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";
}
}

@ -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<Invoice> {
@ -8,9 +7,23 @@ export class Invoice extends Model<Invoice> {
@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;
}

@ -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<Invoice> {
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<any> {

@ -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 {}

@ -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<Invoice> {
try {
if (!userId) {
throw new HttpException("User id not found!", HttpStatus.BAD_REQUEST);
async createInvoiceFromCart(userId: number): Promise<Invoice> {
const user = await User.findByPk(userId);
if (!user) {
throw new HttpException("User not found", HttpStatus.NOT_FOUND);
}
const newInvoice = await this.invoiceModel.create({ userId, totalAmount });
return newInvoice;
} catch (error) {
if (error instanceof HttpException) {
throw error;
const userCartItems = await this.cartService.getUserCart(userId);
if (!userCartItems || userCartItems.cartItems.length === 0) {
throw new HttpException("Cart is empty", HttpStatus.BAD_REQUEST);
}
throw new HttpException("An error occurred while creating the invoice.", HttpStatus.INTERNAL_SERVER_ERROR);
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<Invoice> {
const newInvoice = await this.invoiceModel.create({
userId,
totalAmount,
products,
refId,
paymentStatus,
});
return newInvoice;
}
async getInvoicesByUser(userId: number): Promise<Invoice[]> {
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);
}
}
}

@ -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 };
}
@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' };
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() query: { Authority: string; Status: string; userId: number }): Promise<any> {
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}`);
}
}
}

@ -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],
})

@ -1,15 +1,24 @@
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) {
async requestPayment(amount: number, description: string, callbackUrl: string): Promise<string> {
try {
const result = await this.zarinpal.PaymentRequest({
Amount: amount,
CallbackURL: callbackUrl,
@ -19,10 +28,16 @@ export class PaymentService {
if (result.status === 100) {
return result.url;
} else {
throw new Error(`Error in payment request: ${result.status}`);
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) {
}
async verifyPayment(authority: string, amount: number): Promise<string> {
try {
const result = await this.zarinpal.PaymentVerification({
Amount: amount,
Authority: authority,
@ -31,7 +46,10 @@ export class PaymentService {
if (result.status === 100) {
return result.RefID;
} else {
throw new Error(`Payment verification failed: ${result.status}`);
throw new Error(`Payment verification failed with status: ${result.status}`);
}
} catch (error) {
throw new InternalServerErrorException(`Error in payment verification: ${error.message}`);
}
}
}

@ -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<number> {
@Get(":userId")
async getBalance(@Param("userId") userId: number): Promise<number> {
return this.walletService.getBalance(userId);
}
@Post(':userId/add')
async addBalance(
@Param('userId') userId: number,
@Body('amount') amount: number
): Promise<AddBalanceResponse> {
@Post(":userId/add")
async addBalance(@Param("userId") userId: number, @Body("amount") amount: number): Promise<AddBalanceResponse> {
return this.walletService.addBalance(userId, amount);
}
}

@ -8,5 +8,6 @@ import { SequelizeModule } from '@nestjs/sequelize';
imports: [SequelizeModule.forFeature([Wallet])],
controllers: [WalletController],
providers: [WalletService],
exports: [WalletService],
})
export class WalletModule {}

@ -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<string> {
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";
}
}

Loading…
Cancel
Save