Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[draft] refactor for readable and add tests #18

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6866c9c
feat(test): :wrench: ignore coverage report
lehuygiang28 Jul 12, 2024
84c312d
feat(test): :wrench: config jest test
lehuygiang28 Jul 12, 2024
b1c3592
chore(logger): :sparkles: update to select type of console
lehuygiang28 Jul 13, 2024
2eba6b4
test(logger): :white_check_mark: add test for logger utils
lehuygiang28 Jul 14, 2024
3df7929
fix(utils): :ambulance: param is not used to get date
lehuygiang28 Jul 15, 2024
1d136cf
test(utils): :white_check_mark: add test for common utils
lehuygiang28 Jul 16, 2024
ef72841
refactor(vnpay): :recycle: split to utils function
lehuygiang28 Jul 17, 2024
f80343e
test(utils/payment): :white_check_mark: add test for payment utils
lehuygiang28 Jul 18, 2024
e82655b
test(vnpay): :white_check_mark: add test for verify request
lehuygiang28 Jul 19, 2024
79fc727
fix(utils): :bug: fix wrong used way of timezone
lehuygiang28 Jul 20, 2024
e955cd2
refactor(vnpay): :zap: using util function to get date instead
lehuygiang28 Jul 20, 2024
2de613d
refactor(test): :white_check_mark: update to set date utc correctly
lehuygiang28 Jul 20, 2024
6c97ea7
refactor(test): :white_check_mark: update to set date utc correctly
lehuygiang28 Jul 20, 2024
480240b
refactor(utils): :rocket: update `parseDate` function to support addi…
lehuygiang28 Jul 20, 2024
fd70ed9
fix(utils): :ambulance: remove `.toDate()` due to it alway convert to…
lehuygiang28 Jul 20, 2024
4a0b646
refactor(test): :recycle: update to use `getDateInGMT7` for current time
lehuygiang28 Jul 20, 2024
57d29d4
fix(utils): :ambulance: remove `.toDate()` due to it alway convert to…
lehuygiang28 Jul 20, 2024
859265a
chore(test): :zap: set UTC timezone for Jest tests
lehuygiang28 Jul 20, 2024
e22058f
chore: :zap: update .coderabbit.yaml with tone instructions
lehuygiang28 Jul 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
dist/
lib/
coverage/

docs/.docusaurus/
docs/build/
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
.vscode
lib
.env
.env
coverage/
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ node_modules
src
.gitignore
.prettierrc
coverage/

*.md
!README*.md
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
dist/
lib/
coverage/

docs/.docusaurus/
docs/build/
Expand Down
5 changes: 5 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
coverageThreshold: {
global: {
lines: 80,
},
},
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"build": "rimraf ./lib && tsc",
"prepare": "npm run build",
"test": "jest",
"test:cov": "jest --coverage",
"release": "npm version patch && git push --follow-tags",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down
14 changes: 8 additions & 6 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import timezone from 'moment-timezone';
import { tz } from 'moment-timezone';
import crypto, { BinaryLike } from 'node:crypto';
import { RESPONSE_MAP } from '../constants/response-map.constant';
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved
import { HashAlgorithm, VnpLocale } from '../enums';
import crypto, { BinaryLike } from 'crypto';

export function getDateInGMT7(date: Date): Date {
return timezone(new Date()).tz('Asia/Ho_Chi_Minh').toDate();
export function getDateInGMT7(date?: Date): Date {
return tz(date ?? new Date(), 'Asia/Ho_Chi_Minh').toDate();
}

/**
Expand Down Expand Up @@ -40,7 +40,7 @@ export function dateFormat(date: Date, format = 'yyyyMMddHHmmss'): number {
* @param dateNumber An vnpay date format number
* @returns Date
*/
export function parseDate(dateNumber: number | string): Date {
export function parseDate(dateNumber: number | string, tz: 'utc' | 'local' = 'local'): Date {
const dateString = dateNumber.toString();
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved

const year = parseInt(dateString.slice(0, 4));
Expand All @@ -50,7 +50,9 @@ export function parseDate(dateNumber: number | string): Date {
const minute = parseInt(dateString.slice(10, 12));
const second = parseInt(dateString.slice(12, 14));

return new Date(year, month, day, hour, minute, second);
return tz === 'local'
? new Date(year, month, day, hour, minute, second)
: new Date(Date.UTC(year, month, day, hour, minute, second));
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './common';
export * from './logger';
export * from './payment.util';
8 changes: 5 additions & 3 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as fs from 'fs';
import * as fs from 'node:fs';

/**
* Truyền vào `loggerFn` để bỏ qua logger
Expand All @@ -14,8 +14,10 @@ export function ignoreLogger(): void {}
* @en Log data to console
* @param data - Data to be logged
*/
export function consoleLogger(data: unknown): void {
console.log(data);
export function consoleLogger(data: unknown, symbol: keyof Console = 'log'): void {
if (typeof console[symbol] === 'function') {
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved
(console[symbol] as (...data: unknown[]) => void)(data);
}
}

/**
Expand Down
53 changes: 53 additions & 0 deletions src/utils/payment.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { PAYMENT_ENDPOINT, VNPAY_GATEWAY_SANDBOX_HOST } from '../constants';
import { HashAlgorithm } from '../enums';
import { BuildPaymentUrl, DefaultConfig, GlobalConfig } from '../types';
import { hash, resolveUrlString } from './common';

export function buildPaymentUrlSearchParams(data: Record<string, any>): URLSearchParams {
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved
const searchParams = new URLSearchParams();
Object.entries(data)
.sort(([key1], [key2]) => key1.toString().localeCompare(key2.toString()))
.forEach(([key, value]) => {
// Skip empty value
if (value === '' || value === undefined || value === null) {
return;
}
searchParams.set(key, value.toString());
});
return searchParams;
}

export function createPaymentUrl(
config: Pick<GlobalConfig, 'vnpayHost' | 'paymentEndpoint'>,
data: (BuildPaymentUrl & DefaultConfig) | Record<string, any>,
): URL {
const redirectUrl = new URL(
resolveUrlString(
config.vnpayHost ?? VNPAY_GATEWAY_SANDBOX_HOST,
config.paymentEndpoint ?? PAYMENT_ENDPOINT,
),
);
buildPaymentUrlSearchParams(data).forEach((value, key) => {
redirectUrl.searchParams.set(key, value);
});
return redirectUrl;
}

export function calculateSecureHash(
secureSecret: string,
data: string,
hashAlgorithm: HashAlgorithm,
bufferEncode: BufferEncoding = 'utf-8',
): string {
return hash(secureSecret, Buffer.from(data, bufferEncode), hashAlgorithm);
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved
}

export function verifySecureHash(
secureSecret: string,
data: string,
hashAlgorithm: HashAlgorithm,
receivedHash: string,
): boolean {
const calculatedHash = calculateSecureHash(secureSecret, data, hashAlgorithm);
return calculatedHash === receivedHash;
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved
}
76 changes: 29 additions & 47 deletions src/vnpay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ import { Bank } from './types/bank.type';
import { DefaultConfig, GlobalConfig } from './types/common.type';
import { consoleLogger, ignoreLogger } from './utils';
import { LoggerOptions } from './types/logger.type';
import {
buildPaymentUrlSearchParams,
calculateSecureHash,
createPaymentUrl,
verifySecureHash,
} from './utils/payment.util';

/**
* Lớp hỗ trợ thanh toán qua VNPay
Expand Down Expand Up @@ -184,39 +190,25 @@ export class VNPay {
const dataToBuild = {
...this.defaultConfig,
...data,
};

/**
* Multiply by 100 to follow VNPay standard, see docs for more detail
*/
dataToBuild.vnp_Amount = dataToBuild.vnp_Amount * 100;
/**
* Multiply by 100 to follow VNPay standard, see docs for more detail
*/
vnp_Amount: data.vnp_Amount * 100,
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved
};

if (!isValidVnpayDateFormat(dataToBuild?.vnp_CreateDate ?? 0)) {
const timeGMT7 = timezone(new Date()).tz('Asia/Ho_Chi_Minh').format();
dataToBuild.vnp_CreateDate = dateFormat(new Date(timeGMT7), 'yyyyMMddHHmmss');
}

const redirectUrl = new URL(
resolveUrlString(
this.globalDefaultConfig.vnpayHost ?? VNPAY_GATEWAY_SANDBOX_HOST,
this.globalDefaultConfig.paymentEndpoint ?? PAYMENT_ENDPOINT,
),
);
Object.entries(dataToBuild)
.sort(([key1], [key2]) => key1.toString().localeCompare(key2.toString()))
.forEach(([key, value]) => {
// Skip empty value
if (!value || value === '' || value === undefined || value === null) {
return;
}

redirectUrl.searchParams.append(key, value.toString());
});
const redirectUrl = createPaymentUrl(this.globalDefaultConfig, dataToBuild);
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved

const signed = hash(
const signed = calculateSecureHash(
this.globalDefaultConfig.secureSecret,
Buffer.from(redirectUrl.search.slice(1).toString(), this.BUFFER_ENCODE),
redirectUrl.search.slice(1).toString(),
this.HASH_ALGORITHM,
this.BUFFER_ENCODE,
);
redirectUrl.searchParams.append('vnp_SecureHash', signed);

Expand Down Expand Up @@ -251,49 +243,39 @@ export class VNPay {
query: ReturnQueryFromVNPay,
options?: VerifyReturnUrlOptions<LoggerFields>,
): VerifyReturnUrl {
const { vnp_SecureHash, vnp_SecureHashType, ...cloneQuery } = query;
const { vnp_SecureHash = '', vnp_SecureHashType, ...cloneQuery } = query;

if (typeof cloneQuery?.vnp_Amount !== 'number') {
const res = numberRegex.test(cloneQuery?.vnp_Amount ?? '');
if (!res) {
const isValidAmount = numberRegex.test(cloneQuery?.vnp_Amount ?? '');
if (!isValidAmount) {
throw new Error('Invalid amount');
}
cloneQuery.vnp_Amount = Number(cloneQuery.vnp_Amount);
}

const searchParams = buildPaymentUrlSearchParams(cloneQuery);
const isVerified = verifySecureHash(
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved
lehuygiang28 marked this conversation as resolved.
Show resolved Hide resolved
this.globalDefaultConfig.secureSecret,
searchParams.toString(),
this.HASH_ALGORITHM,
vnp_SecureHash,
);

const outputResults = {
isVerified: true,
isVerified,
isSuccess: cloneQuery.vnp_ResponseCode === '00',
message: getResponseByStatusCode(
cloneQuery.vnp_ResponseCode?.toString() ?? '',
this.globalDefaultConfig.vnp_Locale,
),
};

const searchParams = new URLSearchParams();
Object.entries(cloneQuery)
.sort(([key1], [key2]) => key1.toString().localeCompare(key2.toString()))
.forEach(([key, value]) => {
// Skip empty value
if (value === '' || value === undefined || value === null) {
return;
}

searchParams.append(key, value.toString());
});

const signed = hash(
this.globalDefaultConfig.secureSecret,
Buffer.from(searchParams.toString(), this.BUFFER_ENCODE),
this.HASH_ALGORITHM,
);

if (vnp_SecureHash !== signed) {
if (!isVerified) {
Object.assign(outputResults, {
isVerified: false,
message: 'Wrong checksum',
});
}

const result = {
...cloneQuery,
...outputResults,
Expand Down
Loading
Loading