feat(api): add Vietnam validators and migrate payment services to ConfigService
- Create custom class-validator decorators: IsVietnamPhone, IsVietnamDistrict, IsVND - Replace process.env/requireEnv() with NestJS ConfigService DI in VNPay, MoMo, ZaloPay services - Update all payment infrastructure tests with ConfigService mocks (42 tests passing) TEC-1569 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,35 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { type ConfigService } from '@nestjs/config';
|
||||
import { MomoService } from '../services/momo.service';
|
||||
|
||||
describe('MomoService', () => {
|
||||
let service: MomoService;
|
||||
const secretKey = 'test-momo-secret-key-32chars!!ab';
|
||||
const partnerCode = 'MOMO_TEST';
|
||||
const accessKey = 'test-access-key';
|
||||
const secretKey = 'TESTSECRETKEY123456789012345678';
|
||||
const partnerCode = 'TESTPARTNER';
|
||||
const accessKey = 'TESTACCESSKEY';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('MOMO_PARTNER_CODE', partnerCode);
|
||||
vi.stubEnv('MOMO_ACCESS_KEY', accessKey);
|
||||
vi.stubEnv('MOMO_SECRET_KEY', secretKey);
|
||||
service = new MomoService();
|
||||
const mockConfig = {
|
||||
get: vi.fn((key: string, defaultValue?: string) => {
|
||||
const env: Record<string, string> = {
|
||||
'MOMO_PARTNER_CODE': 'TESTPARTNER',
|
||||
'MOMO_ACCESS_KEY': 'TESTACCESSKEY',
|
||||
'MOMO_SECRET_KEY': 'TESTSECRETKEY123456789012345678',
|
||||
};
|
||||
return env[key] ?? defaultValue;
|
||||
}),
|
||||
getOrThrow: vi.fn((key: string) => {
|
||||
const env: Record<string, string> = {
|
||||
'MOMO_PARTNER_CODE': 'TESTPARTNER',
|
||||
'MOMO_ACCESS_KEY': 'TESTACCESSKEY',
|
||||
'MOMO_SECRET_KEY': 'TESTSECRETKEY123456789012345678',
|
||||
};
|
||||
if (!env[key]) throw new Error(`Missing ${key}`);
|
||||
return env[key];
|
||||
}),
|
||||
} as unknown as ConfigService;
|
||||
service = new MomoService(mockConfig);
|
||||
});
|
||||
|
||||
function buildCallbackData(overrides: Record<string, string> = {}): Record<string, string> {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { type ConfigService } from '@nestjs/config';
|
||||
import { MomoService } from '../services/momo.service';
|
||||
import { PaymentGatewayFactory } from '../services/payment-gateway.factory';
|
||||
import { VnpayService } from '../services/vnpay.service';
|
||||
import { ZalopayService } from '../services/zalopay.service';
|
||||
|
||||
describe('PaymentGatewayFactory', () => {
|
||||
const vnpay = new VnpayService();
|
||||
const momo = new MomoService();
|
||||
const zalopay = new ZalopayService();
|
||||
const mockConfig = {
|
||||
get: vi.fn((key: string, defaultValue?: string) => defaultValue ?? 'test'),
|
||||
getOrThrow: vi.fn(() => 'test-value'),
|
||||
} as unknown as ConfigService;
|
||||
|
||||
const vnpay = new VnpayService(mockConfig);
|
||||
const momo = new MomoService(mockConfig);
|
||||
const zalopay = new ZalopayService(mockConfig);
|
||||
|
||||
const factory = new PaymentGatewayFactory(vnpay, momo, zalopay);
|
||||
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { type ConfigService } from '@nestjs/config';
|
||||
import { VnpayService } from '../services/vnpay.service';
|
||||
|
||||
describe('VnpayService', () => {
|
||||
let service: VnpayService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('VNPAY_TMN_CODE', 'TESTCODE');
|
||||
vi.stubEnv('VNPAY_HASH_SECRET', 'TESTSECRET123456TESTSECRET123456');
|
||||
service = new VnpayService();
|
||||
const mockConfig = {
|
||||
get: vi.fn((key: string, defaultValue?: string) => {
|
||||
const env: Record<string, string> = {
|
||||
VNPAY_TMN_CODE: 'TESTCODE',
|
||||
VNPAY_HASH_SECRET: 'TESTSECRET123456TESTSECRET123456',
|
||||
};
|
||||
return env[key] ?? defaultValue;
|
||||
}),
|
||||
getOrThrow: vi.fn((key: string) => {
|
||||
const env: Record<string, string> = {
|
||||
VNPAY_TMN_CODE: 'TESTCODE',
|
||||
VNPAY_HASH_SECRET: 'TESTSECRET123456TESTSECRET123456',
|
||||
};
|
||||
if (!env[key]) throw new Error(`Missing ${key}`);
|
||||
return env[key];
|
||||
}),
|
||||
} as unknown as ConfigService;
|
||||
service = new VnpayService(mockConfig);
|
||||
});
|
||||
|
||||
it('should create a payment URL', async () => {
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { type ConfigService } from '@nestjs/config';
|
||||
import { ZalopayService } from '../services/zalopay.service';
|
||||
|
||||
describe('ZalopayService', () => {
|
||||
let service: ZalopayService;
|
||||
const key2 = 'test-zalopay-key2-for-callback!!';
|
||||
const key2 = 'TESTKEY2ABCDEF1234567890ABCDEF12';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('ZALOPAY_APP_ID', '2553');
|
||||
vi.stubEnv('ZALOPAY_KEY1', 'test-zalopay-key1-for-signing!!a');
|
||||
vi.stubEnv('ZALOPAY_KEY2', key2);
|
||||
service = new ZalopayService();
|
||||
const mockConfig = {
|
||||
get: vi.fn((key: string, defaultValue?: string) => {
|
||||
const env: Record<string, string> = {
|
||||
'ZALOPAY_APP_ID': '2553',
|
||||
'ZALOPAY_KEY1': 'TESTKEY1ABCDEF1234567890ABCDEF12',
|
||||
'ZALOPAY_KEY2': 'TESTKEY2ABCDEF1234567890ABCDEF12',
|
||||
};
|
||||
return env[key] ?? defaultValue;
|
||||
}),
|
||||
getOrThrow: vi.fn((key: string) => {
|
||||
const env: Record<string, string> = {
|
||||
'ZALOPAY_APP_ID': '2553',
|
||||
'ZALOPAY_KEY1': 'TESTKEY1ABCDEF1234567890ABCDEF12',
|
||||
'ZALOPAY_KEY2': 'TESTKEY2ABCDEF1234567890ABCDEF12',
|
||||
};
|
||||
if (!env[key]) throw new Error(`Missing ${key}`);
|
||||
return env[key];
|
||||
}),
|
||||
} as unknown as ConfigService;
|
||||
service = new ZalopayService(mockConfig);
|
||||
});
|
||||
|
||||
function buildCallbackData(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { type PaymentProvider } from '@prisma/client';
|
||||
import {
|
||||
type IPaymentGateway,
|
||||
@@ -10,33 +11,21 @@ import {
|
||||
type RefundResult,
|
||||
} from './payment-gateway.interface';
|
||||
|
||||
function requireEnv(key: string): string {
|
||||
const value = process.env[key];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MomoService implements IPaymentGateway {
|
||||
private readonly logger = new Logger(MomoService.name);
|
||||
readonly provider: PaymentProvider = 'MOMO';
|
||||
|
||||
private get partnerCode(): string {
|
||||
return requireEnv('MOMO_PARTNER_CODE');
|
||||
}
|
||||
private readonly partnerCode: string;
|
||||
private readonly accessKey: string;
|
||||
private readonly secretKey: string;
|
||||
private readonly endpoint: string;
|
||||
|
||||
private get accessKey(): string {
|
||||
return requireEnv('MOMO_ACCESS_KEY');
|
||||
}
|
||||
|
||||
private get secretKey(): string {
|
||||
return requireEnv('MOMO_SECRET_KEY');
|
||||
}
|
||||
|
||||
private get endpoint(): string {
|
||||
return process.env['MOMO_ENDPOINT'] ?? 'https://test-payment.momo.vn/v2/gateway/api';
|
||||
constructor(private readonly config: ConfigService) {
|
||||
this.partnerCode = this.config.getOrThrow<string>('MOMO_PARTNER_CODE');
|
||||
this.accessKey = this.config.getOrThrow<string>('MOMO_ACCESS_KEY');
|
||||
this.secretKey = this.config.getOrThrow<string>('MOMO_SECRET_KEY');
|
||||
this.endpoint = this.config.get<string>('MOMO_ENDPOINT', 'https://test-payment.momo.vn/v2/gateway/api');
|
||||
}
|
||||
|
||||
async createPaymentUrl(params: CreatePaymentUrlParams): Promise<CreatePaymentUrlResult> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { type PaymentProvider } from '@prisma/client';
|
||||
import {
|
||||
type IPaymentGateway,
|
||||
@@ -10,33 +11,21 @@ import {
|
||||
type RefundResult,
|
||||
} from './payment-gateway.interface';
|
||||
|
||||
function requireEnv(key: string): string {
|
||||
const value = process.env[key];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class VnpayService implements IPaymentGateway {
|
||||
private readonly logger = new Logger(VnpayService.name);
|
||||
readonly provider: PaymentProvider = 'VNPAY';
|
||||
|
||||
private get tmnCode(): string {
|
||||
return requireEnv('VNPAY_TMN_CODE');
|
||||
}
|
||||
private readonly tmnCode: string;
|
||||
private readonly hashSecret: string;
|
||||
private readonly baseUrl: string;
|
||||
private readonly apiUrl: string;
|
||||
|
||||
private get hashSecret(): string {
|
||||
return requireEnv('VNPAY_HASH_SECRET');
|
||||
}
|
||||
|
||||
private get baseUrl(): string {
|
||||
return process.env['VNPAY_BASE_URL'] ?? 'https://sandbox.vnpayment.vn/paymentv2/vpcpay.html';
|
||||
}
|
||||
|
||||
private get apiUrl(): string {
|
||||
return process.env['VNPAY_API_URL'] ?? 'https://sandbox.vnpayment.vn/merchant_webapi/api/transaction';
|
||||
constructor(private readonly config: ConfigService) {
|
||||
this.tmnCode = this.config.getOrThrow<string>('VNPAY_TMN_CODE');
|
||||
this.hashSecret = this.config.getOrThrow<string>('VNPAY_HASH_SECRET');
|
||||
this.baseUrl = this.config.get<string>('VNPAY_BASE_URL', 'https://sandbox.vnpayment.vn/paymentv2/vpcpay.html');
|
||||
this.apiUrl = this.config.get<string>('VNPAY_API_URL', 'https://sandbox.vnpayment.vn/merchant_webapi/api/transaction');
|
||||
}
|
||||
|
||||
async createPaymentUrl(params: CreatePaymentUrlParams): Promise<CreatePaymentUrlResult> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { type PaymentProvider } from '@prisma/client';
|
||||
import {
|
||||
type IPaymentGateway,
|
||||
@@ -10,33 +11,21 @@ import {
|
||||
type RefundResult,
|
||||
} from './payment-gateway.interface';
|
||||
|
||||
function requireEnv(key: string): string {
|
||||
const value = process.env[key];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ZalopayService implements IPaymentGateway {
|
||||
private readonly logger = new Logger(ZalopayService.name);
|
||||
readonly provider: PaymentProvider = 'ZALOPAY';
|
||||
|
||||
private get appId(): string {
|
||||
return requireEnv('ZALOPAY_APP_ID');
|
||||
}
|
||||
private readonly appId: string;
|
||||
private readonly key1: string;
|
||||
private readonly key2: string;
|
||||
private readonly endpoint: string;
|
||||
|
||||
private get key1(): string {
|
||||
return requireEnv('ZALOPAY_KEY1');
|
||||
}
|
||||
|
||||
private get key2(): string {
|
||||
return requireEnv('ZALOPAY_KEY2');
|
||||
}
|
||||
|
||||
private get endpoint(): string {
|
||||
return process.env['ZALOPAY_ENDPOINT'] ?? 'https://sb-openapi.zalopay.vn/v2';
|
||||
constructor(private readonly config: ConfigService) {
|
||||
this.appId = this.config.getOrThrow<string>('ZALOPAY_APP_ID');
|
||||
this.key1 = this.config.getOrThrow<string>('ZALOPAY_KEY1');
|
||||
this.key2 = this.config.getOrThrow<string>('ZALOPAY_KEY2');
|
||||
this.endpoint = this.config.get<string>('ZALOPAY_ENDPOINT', 'https://sb-openapi.zalopay.vn/v2');
|
||||
}
|
||||
|
||||
async createPaymentUrl(params: CreatePaymentUrlParams): Promise<CreatePaymentUrlResult> {
|
||||
|
||||
Reference in New Issue
Block a user