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

@ -1,16 +1,8 @@
"use strict";
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
const tableExists = await queryInterface
.describeTable("Invoices")
.then(() => true)
.catch(() => false);
if (tableExists) {
await queryInterface.dropTable("Invoices", { cascade: true });
}
await queryInterface.createTable("Invoices", {
await queryInterface.createTable('Invoices', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
@ -21,34 +13,45 @@ module.exports = {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: "Users",
key: "id",
model: 'Users',
key: 'id',
},
onDelete: "CASCADE",
onDelete: 'CASCADE',
},
totalPaymentAmount: {
type: Sequelize.FLOAT,
type: Sequelize.INTEGER,
allowNull: false,
},
status: {
type: Sequelize.ENUM("pending", "paid"),
type: Sequelize.ENUM('pending', 'paid'),
allowNull: false,
defaultValue: "pending",
defaultValue: 'pending',
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.fn("NOW"),
defaultValue: Sequelize.fn('NOW'),
},
updatedAt: {
type: Sequelize.DATE,
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) => {
await queryInterface.dropTable("Invoices", { cascade: true });
await queryInterface.dropTable('Invoices');
},
};

@ -21,7 +21,7 @@ module.exports = {
},
productId: {
type: Sequelize.INTEGER,
allowNull: true,
allowNull: false,
references: {
model: 'Products',
key: 'id',
@ -30,9 +30,9 @@ module.exports = {
},
invoiceId: {
type: Sequelize.INTEGER,
allowNull: true,
allowNull: false,
references: {
model: 'Invoices',
model: 'Invoices', // نام جدول Invoices
key: 'id',
},
onDelete: 'CASCADE',
@ -42,7 +42,7 @@ module.exports = {
allowNull: true,
},
productPrice: {
type: Sequelize.DECIMAL(10, 2),
type: Sequelize.INTEGER,
allowNull: true,
},
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)
async processOrder(userId: number, totalAmount: number): Promise<{ message: string; invoice: Invoice }> {
try {
const cart = await this.cartModel.findOne({ where: { userId } });
if (!cart) {
throw new HttpException("Cart not found for this user.", HttpStatus.NOT_FOUND);
const carts = await this.cartModel.findAll({ where: { userId, status: "open" } });
if (!carts || carts.length === 0) {
throw new HttpException("No open carts found for this user.", HttpStatus.NOT_FOUND);
}
// Deducting credit from wallet
await this.walletService.processPayment(userId, totalAmount);
let invoice: Invoice | null = null;
for (const cart of carts) {
const invoiceId = cart.invoiceId;
invoice = await this.invoiceModel.findOne({ where: { id: invoiceId, userId } });
// Retrieve cart items
const cartItems = await this.cartModel.findAll({ where: { userId } });
if (cartItems.length === 0) {
throw new HttpException("Cart is empty.", HttpStatus.BAD_REQUEST);
if (invoice && invoice.status === "paid") {
return {
message: `Order for cart ID ${cart.id} has already been processed.`,
invoice,
};
}
}
// Process each cart item and update stock
for (const cartItem of cartItems) {
await this.walletService.processPayment(userId, totalAmount);
for (const cartItem of carts) {
const { productId, quantity } = cartItem;
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);
}
product.quantity -= quantity; // Reduce stock
product.quantity -= quantity;
await product.save();
}
// Create the invoices for all cart
const invoice = await this.invoiceModel.findOne({ where: { userId, status: "pending" } });
return { message: "Order processed successfully", invoice };
for (const cart of carts) {
cart.status = "closed";
await cart.save();
}
if (invoice) {
invoice.status = "paid";
await invoice.save();
}
return { message: "Order processed successfully!", invoice };
} catch (error) {
console.log(error);
console.error(error);
if (error instanceof HttpException) {
throw error;
} else {

@ -1,8 +1,8 @@
// 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 {
@IsNumber()
@IsInt()
@IsNotEmpty()
productId: number;

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

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

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

@ -3,7 +3,6 @@ import { InjectModel } from "@nestjs/sequelize";
import { Invoice } from "./entities/invoice.entity";
import { CartService } from "src/cart/cart.service";
import { User } from "src/users/entities/user.entity";
import { where } from "sequelize";
@Injectable()
export class InvoiceService {
@ -44,8 +43,7 @@ export class InvoiceService {
await invoice.save();
}
async getInvoiceByUserAndCart(userId: number): Promise<Invoice> {
async getInvoiceByUser(userId: number): Promise<Invoice> {
try {
if (!userId ) {
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 { PaymentService } from "./payment.service";
import { CartService } from "../cart/cart.service";
import { InvoiceService } from "../invoice/invoice.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")
export class PaymentController {
constructor(
private readonly wallet: WalletService,
@InjectModel(Payment) private readonly paymentModel: typeof Payment,
private readonly walletService: WalletService,
private readonly paymentService: PaymentService,
private readonly cartService: CartService,
private readonly invoiceService: InvoiceService,
) {}
@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 invoice = await this.invoiceService.getInvoiceByUser(userId);
const totalAmount = invoice.totalPaymentAmount;
const callbackUrl = `http://localhost:3000/payment/verify?userId=${userId}`;
const paymentUrl = await this.paymentService.requestPayment(totalAmount, "Purchase products", callbackUrl);
return { url: paymentUrl };
@ -39,21 +36,29 @@ export class PaymentController {
if (!userId) {
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 {
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};
await this.walletService.addBalance(userId, totalAmount);
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) {
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}`);
}
}
}

@ -4,9 +4,12 @@ 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';
import { InvoiceModule } from 'src/invoice/invoice.module';
import { Payment } from './entities/payment.entity';
import { SequelizeModule } from '@nestjs/sequelize';
@Module({
imports:[CartModule,WalletModule],
imports:[SequelizeModule.forFeature([Payment]),CartModule,WalletModule,InvoiceModule],
controllers: [PaymentController],
providers: [PaymentService],
})

@ -1,4 +1,6 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Payment } from './entities/payment.entity';
const ZarinpalCheckout = require('zarinpal-checkout');
@ -7,6 +9,7 @@ export class PaymentService {
private zarinpal;
constructor(
) {
this.zarinpal = this.initializeZarinpal();
}
@ -28,6 +31,7 @@ export class PaymentService {
if (result.status === 100) {
return result.url;
} else {
throw new Error(`Payment request failed with status: ${result.status}`);
}
} catch (error) {
@ -42,7 +46,6 @@ export class PaymentService {
Amount: amount,
Authority: authority,
});
if (result.status === 100) {
return result.RefID;
} 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 {
@IsString()
@ -9,7 +9,7 @@ export class CreateProductDto {
@IsNotEmpty()
description: string;
@IsNumber()
@IsInt()
@IsNotEmpty()
price: number;
@ -23,8 +23,7 @@ export class CreateProductDto {
tags?: string[];
@IsOptional()
@IsNumber()
@IsNotEmpty()
@IsInt()
quantity?: number;
@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 {
@IsOptional()
@ -10,7 +10,7 @@ export class UpdateProductDto {
description?: string;
@IsOptional()
@IsNumber()
@IsInt()
price?: number;
@IsOptional()
@ -23,7 +23,7 @@ export class UpdateProductDto {
tags?: string[];
@IsOptional()
@IsNumber()
@IsInt()
quantity?: number;
@IsOptional()

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

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

@ -1,16 +1,12 @@
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")
export class WalletController {
constructor(private readonly walletService: WalletService) {}
@Get(":userId")
async getBalance(@Param("userId") userId: number): Promise<number> {
async getBalance(@Param("userId") userId: number) {
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 {
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 } });
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> {
try {
const wallet = await this.walletModel.findOne({ where: { userId } });
if (wallet) {
wallet.balance += Number(amount);
wallet.balance += amount;
await wallet.save();
return { message: "Balance updated successfully.", balance: wallet.balance };
} else {

Loading…
Cancel
Save