Spaces:
Sleeping
Sleeping
create payment url
Browse files- backend/.env.example +6 -1
- backend/package-lock.json +29 -0
- backend/package.json +3 -0
- backend/src/config/config.ts +5 -0
- backend/src/payment/dto/create-payment.dto.ts +1 -0
- backend/src/payment/dto/update-payment.dto.ts +4 -0
- backend/src/payment/entities/payment.entity.ts +1 -0
- backend/src/payment/payment.controller.spec.ts +20 -0
- backend/src/payment/payment.controller.ts +29 -0
- backend/src/payment/payment.module.ts +9 -0
- backend/src/payment/payment.service.spec.ts +18 -0
- backend/src/payment/payment.service.ts +85 -0
backend/.env.example
CHANGED
@@ -4,4 +4,9 @@ DB_USER='pbl6_jw8s_user'
|
|
4 |
DB_PASSWORD=''
|
5 |
DB_NAME='pbl6_jw8s'
|
6 |
JWT_KEY= ''
|
7 |
-
DB_SSL_ENABLED=true # default is true to connect with remote database
|
|
|
|
|
|
|
|
|
|
|
|
4 |
DB_PASSWORD=''
|
5 |
DB_NAME='pbl6_jw8s'
|
6 |
JWT_KEY= ''
|
7 |
+
DB_SSL_ENABLED=true # default is true to connect with remote database
|
8 |
+
#Payment
|
9 |
+
VNP_TMNCODE = ''
|
10 |
+
VNP_HASHSECRET = ''
|
11 |
+
VNP_URL = ''
|
12 |
+
VNP_RETURNURL = ''
|
backend/package-lock.json
CHANGED
@@ -20,6 +20,7 @@
|
|
20 |
"bcrypt": "^5.1.1",
|
21 |
"class-transformer": "^0.5.1",
|
22 |
"class-validator": "^0.14.1",
|
|
|
23 |
"dotenv": "^16.4.5",
|
24 |
"mysql2": "^3.11.3",
|
25 |
"nest-access-control": "^3.1.0",
|
@@ -36,8 +37,10 @@
|
|
36 |
"@nestjs/cli": "^10.0.0",
|
37 |
"@nestjs/schematics": "^10.0.0",
|
38 |
"@nestjs/testing": "^10.0.0",
|
|
|
39 |
"@types/express": "^4.17.17",
|
40 |
"@types/jest": "^29.5.2",
|
|
|
41 |
"@types/node": "^20.3.1",
|
42 |
"@types/supertest": "^6.0.0",
|
43 |
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
@@ -2144,6 +2147,13 @@
|
|
2144 |
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
|
2145 |
"dev": true
|
2146 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2147 |
"node_modules/@types/estree": {
|
2148 |
"version": "1.0.5",
|
2149 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
@@ -2250,6 +2260,16 @@
|
|
2250 |
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
2251 |
"dev": true
|
2252 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2253 |
"node_modules/@types/node": {
|
2254 |
"version": "20.16.5",
|
2255 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
|
@@ -3933,6 +3953,15 @@
|
|
3933 |
"node": ">= 8"
|
3934 |
}
|
3935 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3936 |
"node_modules/dayjs": {
|
3937 |
"version": "1.11.13",
|
3938 |
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
|
|
20 |
"bcrypt": "^5.1.1",
|
21 |
"class-transformer": "^0.5.1",
|
22 |
"class-validator": "^0.14.1",
|
23 |
+
"dateformat": "^5.0.3",
|
24 |
"dotenv": "^16.4.5",
|
25 |
"mysql2": "^3.11.3",
|
26 |
"nest-access-control": "^3.1.0",
|
|
|
37 |
"@nestjs/cli": "^10.0.0",
|
38 |
"@nestjs/schematics": "^10.0.0",
|
39 |
"@nestjs/testing": "^10.0.0",
|
40 |
+
"@types/dateformat": "^5.0.2",
|
41 |
"@types/express": "^4.17.17",
|
42 |
"@types/jest": "^29.5.2",
|
43 |
+
"@types/multer": "^1.4.12",
|
44 |
"@types/node": "^20.3.1",
|
45 |
"@types/supertest": "^6.0.0",
|
46 |
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
|
2147 |
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
|
2148 |
"dev": true
|
2149 |
},
|
2150 |
+
"node_modules/@types/dateformat": {
|
2151 |
+
"version": "5.0.2",
|
2152 |
+
"resolved": "https://registry.npmjs.org/@types/dateformat/-/dateformat-5.0.2.tgz",
|
2153 |
+
"integrity": "sha512-M95hNBMa/hnwErH+a+VOD/sYgTmo15OTYTM2Hr52/e0OdOuY+Crag+kd3/ioZrhg0WGbl9Sm3hR7UU+MH6rfOw==",
|
2154 |
+
"dev": true,
|
2155 |
+
"license": "MIT"
|
2156 |
+
},
|
2157 |
"node_modules/@types/estree": {
|
2158 |
"version": "1.0.5",
|
2159 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
|
|
2260 |
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
2261 |
"dev": true
|
2262 |
},
|
2263 |
+
"node_modules/@types/multer": {
|
2264 |
+
"version": "1.4.12",
|
2265 |
+
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
|
2266 |
+
"integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
|
2267 |
+
"dev": true,
|
2268 |
+
"license": "MIT",
|
2269 |
+
"dependencies": {
|
2270 |
+
"@types/express": "*"
|
2271 |
+
}
|
2272 |
+
},
|
2273 |
"node_modules/@types/node": {
|
2274 |
"version": "20.16.5",
|
2275 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
|
|
|
3953 |
"node": ">= 8"
|
3954 |
}
|
3955 |
},
|
3956 |
+
"node_modules/dateformat": {
|
3957 |
+
"version": "5.0.3",
|
3958 |
+
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-5.0.3.tgz",
|
3959 |
+
"integrity": "sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA==",
|
3960 |
+
"license": "MIT",
|
3961 |
+
"engines": {
|
3962 |
+
"node": ">=12.20"
|
3963 |
+
}
|
3964 |
+
},
|
3965 |
"node_modules/dayjs": {
|
3966 |
"version": "1.11.13",
|
3967 |
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
backend/package.json
CHANGED
@@ -36,6 +36,7 @@
|
|
36 |
"bcrypt": "^5.1.1",
|
37 |
"class-transformer": "^0.5.1",
|
38 |
"class-validator": "^0.14.1",
|
|
|
39 |
"dotenv": "^16.4.5",
|
40 |
"mysql2": "^3.11.3",
|
41 |
"nest-access-control": "^3.1.0",
|
@@ -52,8 +53,10 @@
|
|
52 |
"@nestjs/cli": "^10.0.0",
|
53 |
"@nestjs/schematics": "^10.0.0",
|
54 |
"@nestjs/testing": "^10.0.0",
|
|
|
55 |
"@types/express": "^4.17.17",
|
56 |
"@types/jest": "^29.5.2",
|
|
|
57 |
"@types/node": "^20.3.1",
|
58 |
"@types/supertest": "^6.0.0",
|
59 |
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
|
36 |
"bcrypt": "^5.1.1",
|
37 |
"class-transformer": "^0.5.1",
|
38 |
"class-validator": "^0.14.1",
|
39 |
+
"dateformat": "^5.0.3",
|
40 |
"dotenv": "^16.4.5",
|
41 |
"mysql2": "^3.11.3",
|
42 |
"nest-access-control": "^3.1.0",
|
|
|
53 |
"@nestjs/cli": "^10.0.0",
|
54 |
"@nestjs/schematics": "^10.0.0",
|
55 |
"@nestjs/testing": "^10.0.0",
|
56 |
+
"@types/dateformat": "^5.0.2",
|
57 |
"@types/express": "^4.17.17",
|
58 |
"@types/jest": "^29.5.2",
|
59 |
+
"@types/multer": "^1.4.12",
|
60 |
"@types/node": "^20.3.1",
|
61 |
"@types/supertest": "^6.0.0",
|
62 |
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
backend/src/config/config.ts
CHANGED
@@ -8,5 +8,10 @@ export const configuration = () => {
|
|
8 |
'db.name': process.env.DB_NAME,
|
9 |
'db.slow_limit': Number(process.env.DB_SLOW_LIMIT) || 500, // ms
|
10 |
'db.ssl_enabled': process.env.DB_SSL_ENABLED,
|
|
|
|
|
|
|
|
|
|
|
11 |
};
|
12 |
};
|
|
|
8 |
'db.name': process.env.DB_NAME,
|
9 |
'db.slow_limit': Number(process.env.DB_SLOW_LIMIT) || 500, // ms
|
10 |
'db.ssl_enabled': process.env.DB_SSL_ENABLED,
|
11 |
+
// payment config
|
12 |
+
'vnp_TmnCode': process.env.VNP_TMNCODE,
|
13 |
+
'vnp_HashSecret': process.env.VNP_HASHSECRET,
|
14 |
+
'vnp_Url': process.env.VNP_URL,
|
15 |
+
'vnp_ReturnUrl': process.env.VNP_RETURNURL,
|
16 |
};
|
17 |
};
|
backend/src/payment/dto/create-payment.dto.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export class CreatePaymentDto {}
|
backend/src/payment/dto/update-payment.dto.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { PartialType } from '@nestjs/mapped-types';
|
2 |
+
import { CreatePaymentDto } from './create-payment.dto';
|
3 |
+
|
4 |
+
export class UpdatePaymentDto extends PartialType(CreatePaymentDto) {}
|
backend/src/payment/entities/payment.entity.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export class Payment {}
|
backend/src/payment/payment.controller.spec.ts
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
2 |
+
import { PaymentController } from './payment.controller';
|
3 |
+
import { PaymentService } from './payment.service';
|
4 |
+
|
5 |
+
describe('PaymentController', () => {
|
6 |
+
let controller: PaymentController;
|
7 |
+
|
8 |
+
beforeEach(async () => {
|
9 |
+
const module: TestingModule = await Test.createTestingModule({
|
10 |
+
controllers: [PaymentController],
|
11 |
+
providers: [PaymentService],
|
12 |
+
}).compile();
|
13 |
+
|
14 |
+
controller = module.get<PaymentController>(PaymentController);
|
15 |
+
});
|
16 |
+
|
17 |
+
it('should be defined', () => {
|
18 |
+
expect(controller).toBeDefined();
|
19 |
+
});
|
20 |
+
});
|
backend/src/payment/payment.controller.ts
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// payment.controller.ts
|
2 |
+
import { Controller, Post, Body, Req, Res } from '@nestjs/common';
|
3 |
+
import { PaymentService } from './payment.service.js';
|
4 |
+
import { Request, Response } from 'express';
|
5 |
+
import { Public } from '../modules/authentication/authentication.decorator.js';
|
6 |
+
|
7 |
+
@Controller('payment')
|
8 |
+
export class PaymentController {
|
9 |
+
constructor(private readonly paymentService: PaymentService) {}
|
10 |
+
|
11 |
+
@Public()
|
12 |
+
@Post('create_payment_url')
|
13 |
+
async createPaymentUrl(@Req() req: Request, @Body() body: any) {
|
14 |
+
console.log("hello")
|
15 |
+
const ipAddr =
|
16 |
+
req.headers['x-forwarded-for'] ||
|
17 |
+
req.socket.remoteAddress ||
|
18 |
+
req.socket?.remoteAddress;
|
19 |
+
console.log(ipAddr);
|
20 |
+
return await this.paymentService.createPaymentUrl(
|
21 |
+
body.amount,
|
22 |
+
body.bankCode,
|
23 |
+
body.orderDescription,
|
24 |
+
body.orderType,
|
25 |
+
body.language,
|
26 |
+
ipAddr as string,
|
27 |
+
);
|
28 |
+
}
|
29 |
+
}
|
backend/src/payment/payment.module.ts
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Module } from '@nestjs/common';
|
2 |
+
import { PaymentService } from './payment.service.js';
|
3 |
+
import { PaymentController } from './payment.controller.js';
|
4 |
+
|
5 |
+
@Module({
|
6 |
+
controllers: [PaymentController],
|
7 |
+
providers: [PaymentService],
|
8 |
+
})
|
9 |
+
export class PaymentModule {}
|
backend/src/payment/payment.service.spec.ts
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
2 |
+
import { PaymentService } from './payment.service';
|
3 |
+
|
4 |
+
describe('PaymentService', () => {
|
5 |
+
let service: PaymentService;
|
6 |
+
|
7 |
+
beforeEach(async () => {
|
8 |
+
const module: TestingModule = await Test.createTestingModule({
|
9 |
+
providers: [PaymentService],
|
10 |
+
}).compile();
|
11 |
+
|
12 |
+
service = module.get<PaymentService>(PaymentService);
|
13 |
+
});
|
14 |
+
|
15 |
+
it('should be defined', () => {
|
16 |
+
expect(service).toBeDefined();
|
17 |
+
});
|
18 |
+
});
|
backend/src/payment/payment.service.ts
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// payment.service.ts
|
2 |
+
import { Injectable } from '@nestjs/common';
|
3 |
+
import { ConfigService } from '@nestjs/config';
|
4 |
+
import * as querystring from 'qs';
|
5 |
+
import * as crypto from 'crypto';
|
6 |
+
|
7 |
+
@Injectable()
|
8 |
+
export class PaymentService {
|
9 |
+
constructor(private readonly configService: ConfigService) {}
|
10 |
+
|
11 |
+
async createPaymentUrl(amount: number, bankCode: string, orderDescription: string, orderType: string, language: string, ipAddr: string) {
|
12 |
+
console.log("hi")
|
13 |
+
const tmnCode = this.configService.get<string>('vnp_TmnCode');
|
14 |
+
const secretKey = this.configService.get<string>('vnp_HashSecret');
|
15 |
+
const vnpUrl = this.configService.get<string>('vnp_Url');
|
16 |
+
const returnUrl = this.configService.get<string>('vnp_ReturnUrl');
|
17 |
+
console.log("1")
|
18 |
+
const date = new Date();
|
19 |
+
const createDate = this.formatDate(date, 'yyyymmddHHmmss');
|
20 |
+
const orderId = "5638"
|
21 |
+
const locale = language || 'vn';
|
22 |
+
const currCode = 'VND';
|
23 |
+
console.log("2")
|
24 |
+
const vnp_Params: Record<string, string> = {
|
25 |
+
vnp_Version: '2.1.0',
|
26 |
+
vnp_Command: 'pay',
|
27 |
+
vnp_TmnCode: tmnCode,
|
28 |
+
vnp_Locale: locale,
|
29 |
+
vnp_CurrCode: currCode,
|
30 |
+
vnp_TxnRef: orderId,
|
31 |
+
vnp_OrderInfo: orderDescription,
|
32 |
+
vnp_OrderType: orderType,
|
33 |
+
vnp_Amount: (amount * 100).toString(),
|
34 |
+
vnp_ReturnUrl: returnUrl,
|
35 |
+
vnp_IpAddr: ipAddr,
|
36 |
+
vnp_CreateDate: createDate,
|
37 |
+
};
|
38 |
+
console.log("3")
|
39 |
+
if (bankCode) {
|
40 |
+
vnp_Params['vnp_BankCode'] = bankCode;
|
41 |
+
}
|
42 |
+
|
43 |
+
// Sort the parameters
|
44 |
+
// const sortedParams = Object.keys(vnp_Params)
|
45 |
+
// .sort()
|
46 |
+
// .reduce((acc, key) => {
|
47 |
+
// acc[key] = vnp_Params[key];
|
48 |
+
// return acc;
|
49 |
+
// }, {} as Record<string, string>);
|
50 |
+
const sortedParams = this.sortObject(vnp_Params);
|
51 |
+
console.log("4")
|
52 |
+
// Sign the data
|
53 |
+
const signData = querystring.stringify(sortedParams, { encode: false });
|
54 |
+
const hmac = crypto.createHmac('sha512', secretKey);
|
55 |
+
const signed = hmac.update(Buffer.from(signData, 'utf-8')).digest('hex');
|
56 |
+
sortedParams['vnp_SecureHash'] = signed;
|
57 |
+
console.log("5")
|
58 |
+
// Create the URL
|
59 |
+
const res = `${vnpUrl}?${querystring.stringify(sortedParams, { encode: false })}`;
|
60 |
+
console.log("6");
|
61 |
+
return res;
|
62 |
+
}
|
63 |
+
// Format date helper function
|
64 |
+
formatDate(date: Date, format: string): string {
|
65 |
+
const yyyymmdd = date.toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
|
66 |
+
const hhmmss = date.toTimeString().slice(0, 8).replace(/:/g, ''); // HHMMSS
|
67 |
+
return format === 'yyyymmddHHmmss' ? yyyymmdd + hhmmss : hhmmss;
|
68 |
+
}
|
69 |
+
|
70 |
+
sortObject(obj) {
|
71 |
+
let sorted = {};
|
72 |
+
let str = [];
|
73 |
+
let key;
|
74 |
+
for (key in obj){
|
75 |
+
if (obj.hasOwnProperty(key)) {
|
76 |
+
str.push(encodeURIComponent(key));
|
77 |
+
}
|
78 |
+
}
|
79 |
+
str.sort();
|
80 |
+
for (key = 0; key < str.length; key++) {
|
81 |
+
sorted[str[key]] = encodeURIComponent(obj[str[key]]).replace(/%20/g, "+");
|
82 |
+
}
|
83 |
+
return sorted;
|
84 |
+
}
|
85 |
+
}
|