Compare commits

..

3 Commits

  1. 17
      migrations/20250104112403-create-product.js
  2. 41
      migrations/20250105062732-create-invoice.js
  3. 8
      migrations/20250105063054-create-cart.js
  4. 43
      migrations/20250108065213-create-wallet.js
  5. 56
      migrations/20250108080238-create-payment-table.js
  6. 46
      src/cart/cart.service.ts
  7. 4
      src/cart/dto/add-to-cart.dto.ts
  8. 2
      src/cart/entities/cart.entity.ts
  9. 2
      src/invoice/entities/invoice.entity.ts
  10. 8
      src/invoice/invoice.controller.ts
  11. 4
      src/invoice/invoice.service.ts
  12. 41
      src/payment/entities/payment.entity.ts
  13. 51
      src/payment/payment.controller.ts
  14. 5
      src/payment/payment.module.ts
  15. 5
      src/payment/payment.service.ts
  16. 7
      src/products/dto/create-product.dto.ts
  17. 6
      src/products/dto/update-product.dto.ts
  18. 2
      src/products/entities/product.entity.ts
  19. 2
      src/wallet/entities/wallet.entity.ts
  20. 8
      src/wallet/wallet.controller.ts
  21. 12
      src/wallet/wallet.service.ts

@ -1,15 +1,14 @@
"use strict"; 'use strict';
module.exports = { module.exports = {
up: async (queryInterface, Sequelize) => { up: async (queryInterface, Sequelize) => {
await queryInterface.dropTable("Products", { cascade: true }); await queryInterface.dropTable('Products', { cascade: true });
await queryInterface.createTable('Products', {
await queryInterface.createTable("Products", {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: false,
autoIncrement: true, autoIncrement: true,
primaryKey: true, primaryKey: true,
allowNull: false,
}, },
name: { name: {
type: Sequelize.STRING, type: Sequelize.STRING,
@ -20,7 +19,7 @@ module.exports = {
allowNull: false, allowNull: false,
}, },
price: { price: {
type: Sequelize.DECIMAL(10, 2), type: Sequelize.INTEGER,
allowNull: false, allowNull: false,
}, },
imageUrl: { imageUrl: {
@ -51,17 +50,17 @@ module.exports = {
createdAt: { createdAt: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.NOW, defaultValue: Sequelize.fn('NOW'),
}, },
updatedAt: { updatedAt: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.NOW, defaultValue: Sequelize.fn('NOW'),
}, },
}); });
}, },
down: async (queryInterface, Sequelize) => { down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable("Products"); await queryInterface.dropTable('Products');
}, },
}; };

@ -1,16 +1,8 @@
"use strict"; 'use strict';
module.exports = { module.exports = {
up: async (queryInterface, Sequelize) => { up: async (queryInterface, Sequelize) => {
const tableExists = await queryInterface await queryInterface.createTable('Invoices', {
.describeTable("Invoices")
.then(() => true)
.catch(() => false);
if (tableExists) {
await queryInterface.dropTable("Invoices", { cascade: true });
}
await queryInterface.createTable("Invoices", {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
autoIncrement: true, autoIncrement: true,
@ -21,34 +13,45 @@ module.exports = {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: "Users", model: 'Users',
key: "id", key: 'id',
}, },
onDelete: "CASCADE", onDelete: 'CASCADE',
}, },
totalPaymentAmount: { totalPaymentAmount: {
type: Sequelize.FLOAT, type: Sequelize.INTEGER,
allowNull: false, allowNull: false,
}, },
status: { status: {
type: Sequelize.ENUM("pending", "paid"), type: Sequelize.ENUM('pending', 'paid'),
allowNull: false, allowNull: false,
defaultValue: "pending", defaultValue: 'pending',
}, },
createdAt: { createdAt: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.fn("NOW"), defaultValue: Sequelize.fn('NOW'),
}, },
updatedAt: { updatedAt: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.fn("NOW"), defaultValue: Sequelize.fn('NOW'),
},
});
await queryInterface.addConstraint('Invoices', {
fields: ['userId'],
type: 'foreign key',
name: 'fk_user_id',
references: {
table: 'Users',
field: 'id',
}, },
onDelete: 'CASCADE',
}); });
}, },
down: async (queryInterface, Sequelize) => { down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable("Invoices", { cascade: true }); await queryInterface.dropTable('Invoices');
}, },
}; };

@ -21,7 +21,7 @@ module.exports = {
}, },
productId: { productId: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: true, allowNull: false,
references: { references: {
model: 'Products', model: 'Products',
key: 'id', key: 'id',
@ -30,9 +30,9 @@ module.exports = {
}, },
invoiceId: { invoiceId: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: true, allowNull: false,
references: { references: {
model: 'Invoices', model: 'Invoices', // نام جدول Invoices
key: 'id', key: 'id',
}, },
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -42,7 +42,7 @@ module.exports = {
allowNull: true, allowNull: true,
}, },
productPrice: { productPrice: {
type: Sequelize.DECIMAL(10, 2), type: Sequelize.INTEGER,
allowNull: true, allowNull: true,
}, },
status: { status: {

@ -0,0 +1,43 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.dropTable("Wallets", { cascade: true });
await queryInterface.createTable('Wallets', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'Users',
key: 'id',
},
onDelete: 'CASCADE',
},
balance: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.fn('NOW'),
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.fn('NOW'),
},
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Wallets');
},
};

@ -0,0 +1,56 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.dropTable("Payments", { cascade: true });
await queryInterface.createTable('Payments', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'Users',
key: 'id',
},
onDelete: 'CASCADE',
},
walletId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'Wallets',
key: 'id',
},
onDelete: 'CASCADE',
},
paymentAmount: {
type: Sequelize.INTEGER,
allowNull: false,
},
status: {
type: Sequelize.ENUM('completed', 'failed'),
allowNull: false,
defaultValue: 'failed',
},
paymentDate: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.fn('NOW'),
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.fn('NOW'),
},
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Payments');
},
};

@ -119,22 +119,28 @@ export class CartService {
//order(clearCart disable) //order(clearCart disable)
async processOrder(userId: number, totalAmount: number): Promise<{ message: string; invoice: Invoice }> { async processOrder(userId: number, totalAmount: number): Promise<{ message: string; invoice: Invoice }> {
try { try {
const cart = await this.cartModel.findOne({ where: { userId } }); const carts = await this.cartModel.findAll({ where: { userId, status: "open" } });
if (!cart) {
throw new HttpException("Cart not found for this user.", HttpStatus.NOT_FOUND); if (!carts || carts.length === 0) {
throw new HttpException("No open carts found for this user.", HttpStatus.NOT_FOUND);
} }
// Deducting credit from wallet let invoice: Invoice | null = null;
await this.walletService.processPayment(userId, totalAmount); for (const cart of carts) {
const invoiceId = cart.invoiceId;
invoice = await this.invoiceModel.findOne({ where: { id: invoiceId, userId } });
// Retrieve cart items if (invoice && invoice.status === "paid") {
const cartItems = await this.cartModel.findAll({ where: { userId } }); return {
if (cartItems.length === 0) { message: `Order for cart ID ${cart.id} has already been processed.`,
throw new HttpException("Cart is empty.", HttpStatus.BAD_REQUEST); invoice,
};
}
} }
// Process each cart item and update stock await this.walletService.processPayment(userId, totalAmount);
for (const cartItem of cartItems) {
for (const cartItem of carts) {
const { productId, quantity } = cartItem; const { productId, quantity } = cartItem;
const product = await this.productModel.findOne({ where: { id: productId } }); const product = await this.productModel.findOne({ where: { id: productId } });
@ -147,15 +153,23 @@ export class CartService {
throw new HttpException(`Insufficient stock for product ID ${productId}.`, HttpStatus.BAD_REQUEST); throw new HttpException(`Insufficient stock for product ID ${productId}.`, HttpStatus.BAD_REQUEST);
} }
product.quantity -= quantity; // Reduce stock product.quantity -= quantity;
await product.save(); await product.save();
} }
// Create the invoices for all cart for (const cart of carts) {
const invoice = await this.invoiceModel.findOne({ where: { userId, status: "pending" } }); cart.status = "closed";
return { message: "Order processed successfully", invoice }; await cart.save();
}
if (invoice) {
invoice.status = "paid";
await invoice.save();
}
return { message: "Order processed successfully!", invoice };
} catch (error) { } catch (error) {
console.log(error); console.error(error);
if (error instanceof HttpException) { if (error instanceof HttpException) {
throw error; throw error;
} else { } else {

@ -1,8 +1,8 @@
// add-to-cart.dto.ts // add-to-cart.dto.ts
import { IsInt, IsNotEmpty, IsNumber, min, Min } from 'class-validator'; import { isInt, IsInt, IsNotEmpty, IsNumber, min, Min } from 'class-validator';
export class AddToCartDto { export class AddToCartDto {
@IsNumber() @IsInt()
@IsNotEmpty() @IsNotEmpty()
productId: number; productId: number;

@ -33,7 +33,7 @@ export class Cart extends Model<Cart> {
quantity: number; quantity: number;
@Column({ @Column({
type: DataType.DECIMAL(10, 2), type: DataType.INTEGER,
allowNull: true, allowNull: true,
}) })
productPrice: number; productPrice: number;

@ -23,7 +23,7 @@ export class Invoice extends Model<Invoice> {
carts: Cart[]; carts: Cart[];
@Column({ @Column({
type: DataType.FLOAT, type: DataType.INTEGER,
allowNull: false, allowNull: false,
}) })
totalPaymentAmount: number; totalPaymentAmount: number;

@ -4,8 +4,8 @@ import { InvoiceService } from "./invoice.service";
@Controller("invoice") @Controller("invoice")
export class InvoiceController { export class InvoiceController {
constructor(private readonly invoiceService: InvoiceService) {} constructor(private readonly invoiceService: InvoiceService) {}
// @Get(":userId") @Get(":userId")
// async getInvoices(@Param("userId") userId: number): Promise<any> { async getInvoices(@Param("userId") userId: number): Promise<any> {
// return this.invoiceService.getInvoicesByUser(userId); return this.invoiceService.getInvoiceByUser(userId);
// } }
} }

@ -3,7 +3,6 @@ import { InjectModel } from "@nestjs/sequelize";
import { Invoice } from "./entities/invoice.entity"; import { Invoice } from "./entities/invoice.entity";
import { CartService } from "src/cart/cart.service"; import { CartService } from "src/cart/cart.service";
import { User } from "src/users/entities/user.entity"; import { User } from "src/users/entities/user.entity";
import { where } from "sequelize";
@Injectable() @Injectable()
export class InvoiceService { export class InvoiceService {
@ -44,8 +43,7 @@ export class InvoiceService {
await invoice.save(); await invoice.save();
} }
async getInvoiceByUser(userId: number): Promise<Invoice> {
async getInvoiceByUserAndCart(userId: number): Promise<Invoice> {
try { try {
if (!userId ) { if (!userId ) {
throw new HttpException("User ID are required.", HttpStatus.BAD_REQUEST); throw new HttpException("User ID are required.", HttpStatus.BAD_REQUEST);

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

@ -1,28 +1,25 @@
import { Controller, Post, Body, Param, Get, Query } from "@nestjs/common"; import { Controller, Post, Body, Param, Get, Query } from "@nestjs/common";
import { PaymentService } from "./payment.service"; import { PaymentService } from "./payment.service";
import { CartService } from "../cart/cart.service";
import { InvoiceService } from "../invoice/invoice.service"; import { InvoiceService } from "../invoice/invoice.service";
import { WalletService } from "src/wallet/wallet.service"; import { WalletService } from "src/wallet/wallet.service";
import { console } from "inspector";
import { InjectModel } from "@nestjs/sequelize";
import { Payment } from "./entities/payment.entity";
@Controller("payment") @Controller("payment")
export class PaymentController { export class PaymentController {
constructor( constructor(
private readonly wallet: WalletService, @InjectModel(Payment) private readonly paymentModel: typeof Payment,
private readonly walletService: WalletService,
private readonly paymentService: PaymentService, private readonly paymentService: PaymentService,
private readonly cartService: CartService, private readonly invoiceService: InvoiceService,
) {} ) {}
@Post("request/:userId") @Post("request/:userId")
async requestPayment(@Param("userId") userId: number): Promise<{ url: string }> { async requestPayment(@Param("userId") userId: number): Promise<{ url: string }> {
const userCartItems = await this.cartService.getUserCart(userId); const invoice = await this.invoiceService.getInvoiceByUser(userId);
const totalAmount = invoice.totalPaymentAmount;
if (!userCartItems || userCartItems.cartItems.length === 0) { const callbackUrl = `http://localhost:3000/payment/verify?userId=${userId}`;
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); const paymentUrl = await this.paymentService.requestPayment(totalAmount, "Purchase products", callbackUrl);
return { url: paymentUrl }; return { url: paymentUrl };
@ -39,21 +36,29 @@ export class PaymentController {
if (!userId) { if (!userId) {
throw new Error("User ID is required."); throw new Error("User ID is required.");
} }
const invoice = await this.invoiceService.getInvoiceByUser(userId);
const totalAmount = invoice.totalPaymentAmount;
const wallet = this.walletService.getBalance(userId);
try { 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); const refId = await this.paymentService.verifyPayment(Authority, totalAmount);
await this.wallet.addBalance(userId,totalAmount) await this.walletService.addBalance(userId, totalAmount);
return { message: "Payment successful", refId}; const wallet = this.walletService.getBalance(userId);
await this.paymentModel.create({
userId,
walletId: (await wallet).walletId,
paymentAmount: totalAmount,
status: "completed",
});
return { message: "Payment successful", refId };
} catch (error) { } catch (error) {
console.log(error) console.log(error);
await this.paymentModel.create({
userId,
walletId: (await wallet).walletId,
paymentAmount: totalAmount,
status: "failed",
});
throw new Error(`Error during payment verification: ${error.message}`); throw new Error(`Error during payment verification: ${error.message}`);
} }
} }
} }

@ -4,9 +4,12 @@ import { PaymentController } from './payment.controller';
import { InvoiceService } from 'src/invoice/invoice.service'; import { InvoiceService } from 'src/invoice/invoice.service';
import { CartModule } from 'src/cart/cart.module'; import { CartModule } from 'src/cart/cart.module';
import { WalletModule } from 'src/wallet/wallet.module'; import { WalletModule } from 'src/wallet/wallet.module';
import { InvoiceModule } from 'src/invoice/invoice.module';
import { Payment } from './entities/payment.entity';
import { SequelizeModule } from '@nestjs/sequelize';
@Module({ @Module({
imports:[CartModule,WalletModule], imports:[SequelizeModule.forFeature([Payment]),CartModule,WalletModule,InvoiceModule],
controllers: [PaymentController], controllers: [PaymentController],
providers: [PaymentService], providers: [PaymentService],
}) })

@ -1,4 +1,6 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Payment } from './entities/payment.entity';
const ZarinpalCheckout = require('zarinpal-checkout'); const ZarinpalCheckout = require('zarinpal-checkout');
@ -7,6 +9,7 @@ export class PaymentService {
private zarinpal; private zarinpal;
constructor( constructor(
) { ) {
this.zarinpal = this.initializeZarinpal(); this.zarinpal = this.initializeZarinpal();
} }
@ -28,6 +31,7 @@ export class PaymentService {
if (result.status === 100) { if (result.status === 100) {
return result.url; return result.url;
} else { } else {
throw new Error(`Payment request failed with status: ${result.status}`); throw new Error(`Payment request failed with status: ${result.status}`);
} }
} catch (error) { } catch (error) {
@ -42,7 +46,6 @@ export class PaymentService {
Amount: amount, Amount: amount,
Authority: authority, Authority: authority,
}); });
if (result.status === 100) { if (result.status === 100) {
return result.RefID; return result.RefID;
} else { } else {

@ -1,4 +1,4 @@
import { IsString, IsNumber, IsOptional, IsNotEmpty, IsArray } from 'class-validator'; import { IsString, IsNumber, IsOptional, IsNotEmpty, IsArray, IsInt } from 'class-validator';
export class CreateProductDto { export class CreateProductDto {
@IsString() @IsString()
@ -9,7 +9,7 @@ export class CreateProductDto {
@IsNotEmpty() @IsNotEmpty()
description: string; description: string;
@IsNumber() @IsInt()
@IsNotEmpty() @IsNotEmpty()
price: number; price: number;
@ -23,8 +23,7 @@ export class CreateProductDto {
tags?: string[]; tags?: string[];
@IsOptional() @IsOptional()
@IsNumber() @IsInt()
@IsNotEmpty()
quantity?: number; quantity?: number;
@IsOptional() @IsOptional()

@ -1,4 +1,4 @@
import { IsString, IsNumber, IsOptional, IsArray } from 'class-validator'; import { IsString, IsNumber, IsOptional, IsArray, IsInt, isInt } from 'class-validator';
export class UpdateProductDto { export class UpdateProductDto {
@IsOptional() @IsOptional()
@ -10,7 +10,7 @@ export class UpdateProductDto {
description?: string; description?: string;
@IsOptional() @IsOptional()
@IsNumber() @IsInt()
price?: number; price?: number;
@IsOptional() @IsOptional()
@ -23,7 +23,7 @@ export class UpdateProductDto {
tags?: string[]; tags?: string[];
@IsOptional() @IsOptional()
@IsNumber() @IsInt()
quantity?: number; quantity?: number;
@IsOptional() @IsOptional()

@ -15,7 +15,7 @@ export class Product extends Model<Product> {
description: string; description: string;
@Column({ @Column({
type: DataType.DECIMAL(10, 2), type: DataType.INTEGER,
allowNull: false, allowNull: false,
}) })
price: number; price: number;

@ -11,7 +11,7 @@ export class Wallet extends Model<Wallet> {
user: User; user: User;
@Column({ @Column({
type: DataType.DECIMAL(10, 2), type: DataType.INTEGER,
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
}) })

@ -1,16 +1,12 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from "@nestjs/common"; import { Controller, Get, Post, Body, Patch, Param, Delete } from "@nestjs/common";
import { WalletService } from "./wallet.service"; import { WalletService } from "./wallet.service";
import { AddBalanceResponse } from "./add-balance-response.interface";
@Controller("wallet") @Controller("wallet")
export class WalletController { export class WalletController {
constructor(private readonly walletService: WalletService) {} constructor(private readonly walletService: WalletService) {}
@Get(":userId") @Get(":userId")
async getBalance(@Param("userId") userId: number): Promise<number> { async getBalance(@Param("userId") userId: number) {
return this.walletService.getBalance(userId); return this.walletService.getBalance(userId);
} }
@Post(":userId/add")
async addBalance(@Param("userId") userId: number, @Body("amount") amount: number): Promise<AddBalanceResponse> {
return this.walletService.addBalance(userId, amount);
}
} }

@ -8,16 +8,22 @@ import { AddBalanceResponse } from "./add-balance-response.interface";
export class WalletService { export class WalletService {
constructor(@InjectModel(Wallet) private walletModel: typeof Wallet) {} constructor(@InjectModel(Wallet) private walletModel: typeof Wallet) {}
async getBalance(userId: number): Promise<number> { async getBalance(userId: number){
const wallet = await this.walletModel.findOne({ where: { userId } }); const wallet = await this.walletModel.findOne({ where: { userId } });
return wallet ? wallet.balance : 0;
if (!wallet) {
throw new HttpException("Wallet not found", HttpStatus.NOT_FOUND);
}
return { walletId:wallet.id, userId:wallet.userId ,balance: wallet.balance };
} }
async addBalance(userId: number, amount: number): Promise<AddBalanceResponse> { async addBalance(userId: number, amount: number): Promise<AddBalanceResponse> {
try { try {
const wallet = await this.walletModel.findOne({ where: { userId } }); const wallet = await this.walletModel.findOne({ where: { userId } });
if (wallet) { if (wallet) {
wallet.balance += Number(amount); wallet.balance += amount;
await wallet.save(); await wallet.save();
return { message: "Balance updated successfully.", balance: wallet.balance }; return { message: "Balance updated successfully.", balance: wallet.balance };
} else { } else {

Loading…
Cancel
Save