feat: payment service with IPG

This commit is contained in:
matin jamshidi
2025-01-03 11:29:33 +03:30
parent f2fb7188ad
commit 6c80c1138f
23 changed files with 495 additions and 96 deletions
+4 -2
View File
@@ -3,7 +3,9 @@ SECRET=fDS9j269DSfd823FhdfG65
ACCESS_TOKEN_SECERT=PNd98dsDS089ds3SDh7C5hg265SFC42bgHxTg34x
ACCESS_TOKEN_SECERT_ADMIN=G89dsJsd90DFdsf96F8se3sgL7JB29265SFC42bgHxTg34x
CONNECTION_STRING=
MERCHANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
MERCHANT_ID=250ae134-635f-4728-8415-ee9a984616e8
IPG_TYPE=sandbox
SITE_URL=http://localhost:3000/payment
EMAIL_HOST=
EMAIL_PORT=
EMAIL_USER=
@@ -23,5 +25,5 @@ STORAGE_ENDPOINT=https://storage.iran.liara.space
STORAGE_BUCKET=dlearn
STORAGE_REGION=default
MONGO_CONNECTION_DANAK=mongodb://root:ahh62bv6o7jndf32@srv-captain--danak-mongo-db:27017/delearn?authSource=admin
MONGO_CONNECTION_DANAK=mongodb://root:3fd87daf47fd07183a424a998d929307@89.32.249.24:32768/delearn?authSource=admin&replicaSet=rs0&directConnection=true
NODE_ENV=production
+2 -2
View File
@@ -3,7 +3,7 @@ SECRET=fDS9j269DSfd823FhdfG65
ACCESS_TOKEN_SECERT=PNd98dsDS089ds3SDh7C5hg265SFC42bgHxTg34x
ACCESS_TOKEN_SECERT_ADMIN=G89dsJsd90DFdsf96F8se3sgL7JB29265SFC42bgHxTg34x
CONNECTION_STRING=
MERCHANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
MERCHANT_ID=250ae134-635f-4728-8415-ee9a984616e8
EMAIL_HOST=
EMAIL_PORT=
EMAIL_USER=
@@ -24,5 +24,5 @@ STORAGE_SECRET_KEY=95e78068-ff60-41c0-82c3-71a0de0a3e2f
STORAGE_ENDPOINT=https://storage.iran.liara.space
STORAGE_BUCKET=dlearn
STORAGE_REGION=default
MONGO_CONNECTION_DANAK=mongodb://root:ahh62bv6o7jndf32@srv-captain--danak-mongo-db:27017/delearn?authSource=admin
MONGO_CONNECTION_DANAK=mongodb://root:3fd87daf47fd07183a424a998d929307@89.32.249.24:32768/delearn?authSource=admin&replicaSet=rs0&directConnection=true
NODE_ENV=production
+48
View File
@@ -0,0 +1,48 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsMongoId, IsNotEmpty } from "class-validator";
export function paymentObj(payment) {
return {
id: payment.id,
course: payment.course,
authority: payment.authority,
amount: payment.amount,
user: payment.user,
createdAt: payment.createdAt,
updatedAt: payment.updatedAt,
};
}
export class PaymentDto {
@IsNotEmpty()
@ApiProperty({ type: String })
@IsMongoId({ message: 'آیدی نامعتبر است.' })
id: string;
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the course id ", example: "66d9ab8b45818616a48bc322" })
courseId: string;
@IsNotEmpty()
@ApiProperty({ type: String })
authority: string;
@ApiProperty({ type: Number })
amount: number;
@ApiProperty({ type: Date })
createdAt: Date;
@ApiProperty({ type: Date })
updatedAt: Date;
}
export class CreatePaymentDto {
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the course id ", example: "66d9ab8b45818616a48bc322" })
courseId: string;
@IsNotEmpty()
@ApiProperty({ type: Number, description: "the amount of payment", example: 10000 })
amount: number;
}
+3 -1
View File
@@ -12,6 +12,7 @@ import { UploaderModule } from './application/common/uploader/uploader.module';
import { CourseHeadlineModule } from './application/courseHeadline/courseHeadline.module';
import { ConfigModule } from '@nestjs/config';
import { CourseCategoryModule } from './application/courseCategory/courseCategory.module';
import { PaymentModule } from './application/payment/payment.module';
@Module({
imports: [
@@ -34,6 +35,7 @@ import { CourseCategoryModule } from './application/courseCategory/courseCategor
UploaderModule,
CourseHeadlineModule,
CourseCategoryModule,
PaymentModule,
// ServeStaticModule.forRoot({
// rootPat h: join(__dirname, '..', 'public'),
// }),
@@ -49,4 +51,4 @@ import { CourseCategoryModule } from './application/courseCategory/courseCategor
providers: [UploaderService],
exports: [UploaderService],
})
export class AppModule {}
export class AppModule { }
+18
View File
@@ -0,0 +1,18 @@
import { Injectable } from "@nestjs/common";
import { ZarinPalGateway } from "./gateways/zarinpal";
@Injectable()
export class PaymentGateway {
constructor(
private zarinpalGateway: ZarinPalGateway,
) { }
async requestPayment(data: any) {
return await this.zarinpalGateway.processPayment(data.amount, data.userId, data.callbackPath);
}
async verifyPayment(data: any) {
return await this.zarinpalGateway.verifyPayment(data.authority, data.amount);
}
}
+59
View File
@@ -0,0 +1,59 @@
import { Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
import axios from "axios";
@Injectable()
export class ZarinPalGateway {
private logger = new Logger(ZarinPalGateway.name)
private gatewayApiURL: string = `https://${process.env.IPG_TYPE}.zarinpal.com/pg`;
private callbackURL: string = `${process.env.SITE_URL}/verify/Zarinpal`;
private merchantID: string = process.env.MERCHANT_ID;
private requestHeaders: any = {
'Content-Type': 'application/json',
Accept: 'application/json',
};
async processPayment(amount: number, userId: string, callbackPath: string) {
try {
const purchaseData = {
merchant_id: this.merchantID,
amount,
callback_url: `${this.callbackURL}/${callbackPath}`,
description: "خرید دوره",
currency: "IRR" as const,
metadata: { userId },
}
const response = await axios.post(`${this.gatewayApiURL}/v4/payment/request.json`, purchaseData, { headers: this.requestHeaders });
const { data } = response.data;
console.log(data)
return {
redirectUrl: `${this.gatewayApiURL}/StartPay/${data.authority}`,
message: data.message,
authority: data.authority,
}
} catch (error) {
console.log(error)
this.logger.error("error in payment process", error);
throw new InternalServerErrorException("error in payment process");
}
}
async verifyPayment(authority: string, amount: number) {
try {
const verifyData = {
merchant_id: this.merchantID,
amount,
authority,
}
const response = await axios.post(`${this.gatewayApiURL}/v4/payment/verify.json`, verifyData, { headers: this.requestHeaders });
const { data } = response.data;
return data;
} catch (error) {
this.logger.error("error in payment verification", error);
throw new InternalServerErrorException("error in payment verification");
}
}
}
+12
View File
@@ -0,0 +1,12 @@
export interface NewPaymentData {
amount: number;
description: string;
email: string;
mobile: string;
callbackPath: string;
}
export interface VerifyPaymentData {
authority: string;
amount: number;
}
+1 -1
View File
@@ -23,7 +23,7 @@ export class CourseService {
private courseCategoryDataAccess: CourseCategoryDataAccess,
private commentDataAccess: CommentDataAccess,
private courseHeadlineDataAccess: CourseHeadlineDataAccess,
) {}
) { }
async createCourse(
createCourseDto: CreateCourseDto,
@@ -0,0 +1,66 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiBody,
ApiOperation,
ApiQuery,
} from '@nestjs/swagger';
import { CurrentUser } from '../../decorators/currentUser.decorator';
import { AuthGuard } from '../../auth/auth.guard';
import { ApiTags } from '@nestjs/swagger';
import { Roles } from '../../decorators/role.decorators';
import { Role } from '../../common/eNums/role.enum';
import { PaymentService } from './payment.service';
import { CreatePaymentDto } from 'src/DTO/payment.dto';
@ApiTags('payment')
@Controller('payment')
export class PaymentController {
constructor(
private readonly paymentService: PaymentService,
) { }
@ApiOperation({
summary: 'ایجاد درخواست پرداخت',
})
@ApiBody({
type: CreatePaymentDto,
description: 'get body',
})
@UseGuards(AuthGuard)
@ApiBearerAuth()
@Roles(Role.User)
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() createPaymentDto: CreatePaymentDto, @CurrentUser() user) {
return this.paymentService.createPayment(user.id, createPaymentDto.amount, createPaymentDto.courseId);
}
@ApiOperation({
summary: 'تایید پرداخت',
})
@ApiQuery({
name: 'Authority',
type: 'string',
description: 'get authority',
})
@ApiQuery({
name: 'status',
type: 'string',
description: 'get status',
})
@Get('/verify/Zarinpal/cart')
async verifyPayment(@Query('Authority') authority: string, @Query('Status') status: string) {
console.log({ authorityInController: authority, status })
return this.paymentService.verifyPayment(authority, status);
}
}
+24
View File
@@ -0,0 +1,24 @@
import { Logger, Module } from '@nestjs/common';
import { UserModule } from '../user/user.module';
import { UserDataAccess } from '../../dataAccess/user.dataAccess';
import { PaymentService } from './payment.service';
import { PaymentController } from './payment.controller';
import { PaymentDataAccess } from 'src/dataAccess/payment.dataAccess';
import { PaymentGateway } from '../IPG/PaymentGateway';
import { ZarinPalGateway } from '../IPG/gateways/zarinpal';
@Module({
imports: [
UserModule,
],
controllers: [PaymentController],
providers: [
ZarinPalGateway,
PaymentDataAccess,
PaymentGateway,
PaymentService,
UserDataAccess,
],
exports: []
})
export class PaymentModule { }
@@ -0,0 +1,63 @@
import { Injectable, Logger } from "@nestjs/common";
import { PaymentDataAccess } from "../../dataAccess/payment.dataAccess";
import { startSession } from "mongoose";
import { PaymentGateway } from "../IPG/PaymentGateway";
import { UserDataAccess } from "../../dataAccess/user.dataAccess";
@Injectable()
export class PaymentService {
private readonly logger = new Logger(PaymentService.name);
constructor(
private readonly paymentDataAccess: PaymentDataAccess,
private readonly paymentGateway: PaymentGateway,
private readonly userDataAccess: UserDataAccess,
) { }
async createPayment(userId: string, amount: number, courseId: string) {
const session = await startSession();
session.startTransaction();
try {
const paymentData = await this.paymentGateway.requestPayment({ amount, userId, callbackPath: "cart" });
console.log(paymentData)
if (!paymentData) {
throw new Error("Error in payment process");
}
const payment = await this.paymentDataAccess.createPayment(
userId,
amount,
courseId,
paymentData.authority,
session
);
console.log({ payment })
await session.commitTransaction();
await session.endSession();
return { payment: payment, paymentData }
} catch (error) {
await session.abortTransaction();
await session.endSession();
this.logger.warn(error);
}
}
async verifyPayment(authority: string, status: string) {
console.log({ authorityInService: authority })
const paymentData = await this.paymentDataAccess.getPaymentByAuthority(authority);
console.log({ paymentData })
if (!paymentData) {
throw new Error("payment not found");
}
const verifyData = await this.paymentGateway.verifyPayment({ authority, amount: paymentData.amount });
if (verifyData.code === 100 || verifyData.code === 101) {
const registerCourse = await this.userDataAccess.registerCourse(paymentData.userId, paymentData.courseId);
console.log(registerCourse)
if (!registerCourse.success) {
throw new Error("error in register course");
}
return { success: true, payment: paymentData, verifyData }
}
}
}
+40 -40
View File
@@ -30,7 +30,7 @@ import { CurrentUser } from '../../decorators/currentUser.decorator';
@ApiTags('user')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
constructor(private readonly userService: UserService) { }
// profile user ************************************
@ApiOperation({
@@ -110,45 +110,45 @@ export class UserController {
}
}
@ApiOperation({
summary: 'افزایش موجودی کیف پول',
})
@ApiBody({
type: AddBalanceDto,
description: 'get body',
})
@UseGuards(AuthGuard)
@Roles(Role.User)
@ApiBearerAuth()
@Post('add')
addBalance(@CurrentUser() user, @Body() addBalanceDto: AddBalanceDto) {
return this.userService.addBalance(
user.id,
addBalanceDto.amount,
addBalanceDto.paymentNumber,
addBalanceDto.reason,
);
}
// @ApiOperation({
// summary: 'افزایش موجودی کیف پول',
// })
// @ApiBody({
// type: AddBalanceDto,
// description: 'get body',
// })
// @UseGuards(AuthGuard)
// @Roles(Role.User)
// @ApiBearerAuth()
// @Post('add')
// addBalance(@CurrentUser() user, @Body() addBalanceDto: AddBalanceDto) {
// return this.userService.addBalance(
// user.id,
// addBalanceDto.amount,
// addBalanceDto.paymentNumber,
// addBalanceDto.reason,
// );
// }
@ApiOperation({
summary: 'موجودی کیف پول',
})
@UseGuards(AuthGuard)
@Roles(Role.User)
@ApiBearerAuth()
@Get('balance')
getBalance(@CurrentUser() user) {
return this.userService.getBalance(user.id);
}
// @ApiOperation({
// summary: 'موجودی کیف پول',
// })
// @UseGuards(AuthGuard)
// @Roles(Role.User)
// @ApiBearerAuth()
// @Get('balance')
// getBalance(@CurrentUser() user) {
// return this.userService.getBalance(user.id);
// }
@ApiOperation({
summary: 'لیست تراکنش ها',
})
@UseGuards(AuthGuard)
@Roles(Role.User)
@ApiBearerAuth()
@Get('payments')
getPayments(@CurrentUser() user) {
return this.userService.getPayments(user.id);
}
// @ApiOperation({
// summary: 'لیست تراکنش ها',
// })
// @UseGuards(AuthGuard)
// @Roles(Role.User)
// @ApiBearerAuth()
// @Get('payments')
// getPayments(@CurrentUser() user) {
// return this.userService.getPayments(user.id);
// }
}
+35 -35
View File
@@ -21,7 +21,7 @@ export class UserService {
private readonly userDiscountDataAccess: UserDiscountDataAccess,
private readonly discountHistoryDataAccess: DiscountHistoryDataAccess,
private readonly paymentDataAccess: PaymentDataAccess,
) {}
) { }
async profile(id) {
const result = await this.userDataAccess.findOne(id);
@@ -150,40 +150,40 @@ export class UserService {
);
}
async addBalance(
userId: string,
amount: number,
paymentNumber: string,
reason: string,
) {
const user = await this.userDataAccess.findOne(userId);
const payment = await this.paymentDataAccess.createPayment(
userId,
amount,
paymentNumber,
reason,
);
user.walletBalance += amount;
return await user.save();
}
// async addBalance(
// userId: string,
// amount: number,
// paymentNumber: string,
// reason: string,
// ) {
// const user = await this.userDataAccess.findOne(userId);
// const payment = await this.paymentDataAccess.createPayment(
// userId,
// amount,
// paymentNumber,
// reason,
// );
// user.walletBalance += amount;
// return await user.save();
// }
async getBalance(userId: string): Promise<number> {
const user = await this.userDataAccess.findOne(userId);
return user.walletBalance;
}
// async getBalance(userId: string): Promise<number> {
// const user = await this.userDataAccess.findOne(userId);
// return user.walletBalance;
// }
async getPayments(userId: string) {
const user = await this.userDataAccess.findOne(userId);
if (!user) {
throw new HttpException(
{
status: HttpStatus.NOT_FOUND,
error: 'کاربر وجود ندارد',
},
HttpStatus.NOT_FOUND,
);
}
const payments = await this.paymentDataAccess.getPaymentsByUserId(userId);
return payments;
}
// async getPayments(userId: string) {
// const user = await this.userDataAccess.findOne(userId);
// if (!user) {
// throw new HttpException(
// {
// status: HttpStatus.NOT_FOUND,
// error: 'کاربر وجود ندارد',
// },
// HttpStatus.NOT_FOUND,
// );
// }
// const payments = await this.paymentDataAccess.getPaymentsByUserId(userId);
// return payments;
// }
}
+5
View File
@@ -0,0 +1,5 @@
export enum PaymentStatus {
Pending = "Pending",
Cancelled = "Cancelled",
Completed = "Completed",
}
+6
View File
@@ -217,6 +217,12 @@ export class CourseDataAccess {
return result;
}
async getUserCourses(user: string): Promise<Course[]> {
const result = await Models.Course.find({ studentIds: user });
return result
}
async deleteCourse(courseId: string): Promise<Course> {
const result = await Models.Course.findByIdAndDelete(courseId);
return result;
+10 -2
View File
@@ -2,8 +2,10 @@ import Models from "../models";
import Payment from "../models/payment.model";
export class PaymentDataAccess {
async createPayment(userId: string, amount: number, paymentNumber: string,reason:string):Promise<Payment> {
const payment = Models.Payment.create({ userId, amount, paymentNumber,reason });
async createPayment(userId: string, amount: number, courseId: string, authority: string, session: any): Promise<Payment> {
const paymentDoc = await Models.Payment.create([{ userId, amount, courseId, authority }], { session });
console.log({ paymentDoc });
const payment = paymentDoc.map(doc => doc.toObject())[0];
return payment;
}
@@ -12,6 +14,12 @@ export class PaymentDataAccess {
return payment;
}
async getPaymentByAuthority(authority: string): Promise<Payment | null> {
console.log({ authority })
const payment = await Models.Payment.findOne({ authority });
console.log(payment, "ssssss")
return payment;
}
async getPaymentsByUserId(userId: string): Promise<Payment[]> {
const payments = await Models.Payment.find({ userId });
return payments;
+25
View File
@@ -0,0 +1,25 @@
import { Purchase } from "../models/schemas/purchase.schema";
import Models from "../models";
export class PurchaseDataAccess {
async createPurchase(userId: string, courseId: string, paymentId: string): Promise<Purchase> {
const purchase = Models.Purchase.create({ userId, courseId, paymentId });
return purchase;
}
async getPurchase(purchaseId: string): Promise<Purchase | null> {
const purchase = await Models.Purchase.findById(purchaseId);
return purchase;
}
async getPurchasesByUserId(userId: string): Promise<Purchase[]> {
const purchase = await Models.Purchase.find({ userId });
return purchase;
}
async getPurchases(): Promise<Purchase[]> {
const purchase = await Models.Purchase.find();
return purchase;
}
}
+1 -1
View File
@@ -112,6 +112,6 @@ export class UserDataAccess {
course.studentIds.push(userId);
await user.save();
await course.save();
return { message: 'ثبت نام با موفقیت انجام شد' };
return { message: 'ثبت نام با موفقیت انجام شد', success: true };
}
}
+2 -1
View File
@@ -5,7 +5,7 @@ import mongoose from 'mongoose';
import * as dotenv from 'dotenv';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import * as swaggerConfig from './config/swagger.json';
import { ValidationPipe } from '@nestjs/common';
import { Logger, ValidationPipe } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import paths from './config/paths';
dotenv.config();
@@ -61,5 +61,6 @@ async function bootstrap() {
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/v1', app, document);
await app.listen(3000, '0.0.0.0');
Logger.log(`Server api docs running on http://localhost:3000/api/v1`, 'Boostrap');
}
bootstrap();
+2
View File
@@ -19,6 +19,7 @@ import UserDiscount from './userDiscount.model';
import Rate from './rate.model';
import Payment from './payment.model';
import Settings from './settings.model';
import Purchase from './Purchase.model';
const Models = {
User,
@@ -42,6 +43,7 @@ const Models = {
CourseHeadlineComment,
Rate,
Payment,
Purchase
};
export default Models;
+12
View File
@@ -0,0 +1,12 @@
import mongoose from 'mongoose';
import { softDeletePlugin, SoftDeleteModel } from 'soft-delete-plugin-mongoose';
import { cleanJson } from './plugins';
import { Purchase, PurchaseSchema } from './schemas/purchase.schema';
PurchaseSchema.plugin(softDeletePlugin);
PurchaseSchema.plugin(cleanJson);
const Purchase = mongoose.model<Purchase, SoftDeleteModel<Purchase>>(
'purchases',
PurchaseSchema,
);
export default Purchase;
+22 -9
View File
@@ -1,5 +1,6 @@
import mongoose from "mongoose";
import { PaymentStatus } from "src/common/eNums/paymentStatus.enum";
export const PaymentSchema = new mongoose.Schema({
userId: {
@@ -7,31 +8,43 @@ export const PaymentSchema = new mongoose.Schema({
ref: 'users',
required: true
},
courseId: {
type: mongoose.Types.ObjectId,
ref: 'courses',
required: true
},
authority: {
type: String,
required: true
},
amount: {
type: Number,
required: true
},
paymentNumber: {
transactionNumber: {
type: String,
required: true,
default: null
},
reason: {
paymentStatus: {
type: String,
enum: ['deposit', 'withdrawal'],
enum: PaymentStatus,
default: PaymentStatus.Pending,
required: true
},
},
{
},
{
collection: 'payments',
timestamps: true,
},
},
);
export interface Payment extends mongoose.Document {
userId: mongoose.Types.ObjectId;
amount: number;
paymentNumber: string;
reason: 'deposit' | 'withdrawal';
authority: string;
courseId: mongoose.Types.ObjectId;
transactionNumber: string;
paymentStatus: PaymentStatus;
createdAt: Date;
updatedAt: Date;
}
+33
View File
@@ -0,0 +1,33 @@
import mongoose from "mongoose";
export const PurchaseSchema = new mongoose.Schema({
userId: {
type: mongoose.Types.ObjectId,
ref: 'users',
required: true
},
courseId: {
type: mongoose.Types.ObjectId,
ref: 'courses',
required: true
},
paymentId: {
type: mongoose.Types.ObjectId,
ref: 'payments',
required: true
},
},
{
collection: 'purchases',
timestamps: true,
},
);
export interface Purchase extends mongoose.Document {
userId: mongoose.Types.ObjectId;
courseId: mongoose.Types.ObjectId;
paymentId: mongoose.Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}