test(e2e): add 14 new web E2E test files for critical user flows
Cover auth (login, register, OAuth callbacks), search with filters, listing detail, dashboard, analytics, create listing form, admin dashboard/users/moderation/KYC, navigation routing, and responsive design. Total 91 test cases using Playwright with API route mocking. Also fix mcp-servers tsconfig deprecation warning for TS 7.x compat. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { type IPaymentRepository } from '../../domain/repositories/payment.repository';
|
||||
import { Money } from '../../domain/value-objects/money.vo';
|
||||
import { type IPaymentGatewayFactory } from '../../infrastructure/services/payment-gateway.interface';
|
||||
import { CreatePaymentCommand } from '../commands/create-payment/create-payment.command';
|
||||
import { CreatePaymentHandler } from '../commands/create-payment/create-payment.handler';
|
||||
|
||||
describe('CreatePaymentHandler', () => {
|
||||
let handler: CreatePaymentHandler;
|
||||
let mockPaymentRepo: { [K in keyof IPaymentRepository]: ReturnType<typeof vi.fn> };
|
||||
let mockGatewayFactory: { getGateway: ReturnType<typeof vi.fn> };
|
||||
let mockGateway: { createPaymentUrl: ReturnType<typeof vi.fn>; verifyCallback: ReturnType<typeof vi.fn>; refund: ReturnType<typeof vi.fn> };
|
||||
let mockEventBus: { publish: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockPaymentRepo = {
|
||||
findById: vi.fn(),
|
||||
findByProviderTxId: vi.fn(),
|
||||
findByIdempotencyKey: vi.fn(),
|
||||
findByUserId: vi.fn(),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn(),
|
||||
updateIfStatus: vi.fn(),
|
||||
};
|
||||
|
||||
mockGateway = {
|
||||
createPaymentUrl: vi.fn().mockResolvedValue({
|
||||
paymentUrl: 'https://vnpay.vn/pay/123',
|
||||
providerTxId: 'vnpay-tx-1',
|
||||
}),
|
||||
verifyCallback: vi.fn(),
|
||||
refund: vi.fn(),
|
||||
};
|
||||
|
||||
mockGatewayFactory = {
|
||||
getGateway: vi.fn().mockReturnValue(mockGateway),
|
||||
};
|
||||
|
||||
mockEventBus = { publish: vi.fn() };
|
||||
|
||||
handler = new CreatePaymentHandler(
|
||||
mockPaymentRepo as any,
|
||||
mockGatewayFactory as any,
|
||||
mockEventBus as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates payment successfully', async () => {
|
||||
mockPaymentRepo.findByIdempotencyKey.mockResolvedValue(null);
|
||||
|
||||
const command = new CreatePaymentCommand(
|
||||
'user-1', 'VNPAY', 'SUBSCRIPTION', 500_000n,
|
||||
'Thanh toán gói Pro', 'https://goodgo.vn/return', '127.0.0.1',
|
||||
undefined, 'idem-key-1',
|
||||
);
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.paymentId).toBeDefined();
|
||||
expect(result.paymentUrl).toBe('https://vnpay.vn/pay/123');
|
||||
expect(result.providerTxId).toBe('vnpay-tx-1');
|
||||
expect(mockPaymentRepo.save).toHaveBeenCalledTimes(1);
|
||||
expect(mockEventBus.publish).toHaveBeenCalled();
|
||||
expect(mockGatewayFactory.getGateway).toHaveBeenCalledWith('VNPAY');
|
||||
});
|
||||
|
||||
it('throws ConflictException for duplicate idempotency key (pending)', async () => {
|
||||
mockPaymentRepo.findByIdempotencyKey.mockResolvedValue({ status: 'PENDING' });
|
||||
|
||||
const command = new CreatePaymentCommand(
|
||||
'user-1', 'VNPAY', 'SUBSCRIPTION', 500_000n,
|
||||
'desc', 'https://goodgo.vn/return', '127.0.0.1',
|
||||
undefined, 'existing-key',
|
||||
);
|
||||
|
||||
await expect(handler.execute(command)).rejects.toThrow(/idempotency/);
|
||||
});
|
||||
|
||||
it('throws ConflictException for already processed idempotency key', async () => {
|
||||
mockPaymentRepo.findByIdempotencyKey.mockResolvedValue({ status: 'COMPLETED' });
|
||||
|
||||
const command = new CreatePaymentCommand(
|
||||
'user-1', 'VNPAY', 'SUBSCRIPTION', 500_000n,
|
||||
'desc', 'https://goodgo.vn/return', '127.0.0.1',
|
||||
undefined, 'completed-key',
|
||||
);
|
||||
|
||||
await expect(handler.execute(command)).rejects.toThrow(/xử lý/);
|
||||
});
|
||||
|
||||
it('throws ValidationException for invalid amount', async () => {
|
||||
const command = new CreatePaymentCommand(
|
||||
'user-1', 'VNPAY', 'SUBSCRIPTION', -100n,
|
||||
'desc', 'https://goodgo.vn/return', '127.0.0.1',
|
||||
);
|
||||
|
||||
await expect(handler.execute(command)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('creates payment without idempotency key', async () => {
|
||||
const command = new CreatePaymentCommand(
|
||||
'user-1', 'VNPAY', 'DEPOSIT', 1_000_000n,
|
||||
'Nạp tiền', 'https://goodgo.vn/return', '127.0.0.1',
|
||||
);
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.paymentId).toBeDefined();
|
||||
expect(mockPaymentRepo.findByIdempotencyKey).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { PaymentEntity } from '../../domain/entities/payment.entity';
|
||||
import { type IPaymentRepository } from '../../domain/repositories/payment.repository';
|
||||
import { Money } from '../../domain/value-objects/money.vo';
|
||||
import { GetPaymentStatusQuery } from '../queries/get-payment-status/get-payment-status.query';
|
||||
import { GetPaymentStatusHandler } from '../queries/get-payment-status/get-payment-status.handler';
|
||||
|
||||
function createPayment(): PaymentEntity {
|
||||
const money = Money.create(500_000n).unwrap();
|
||||
const payment = PaymentEntity.createNew('pay-1', 'user-1', 'VNPAY', 'SUBSCRIPTION', money);
|
||||
payment.markProcessing('vnpay-tx-1');
|
||||
payment.clearDomainEvents();
|
||||
return payment;
|
||||
}
|
||||
|
||||
describe('GetPaymentStatusHandler', () => {
|
||||
let handler: GetPaymentStatusHandler;
|
||||
let mockPaymentRepo: { [K in keyof IPaymentRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockPaymentRepo = {
|
||||
findById: vi.fn(),
|
||||
findByProviderTxId: vi.fn(),
|
||||
findByIdempotencyKey: vi.fn(),
|
||||
findByUserId: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
updateIfStatus: vi.fn(),
|
||||
};
|
||||
|
||||
handler = new GetPaymentStatusHandler(mockPaymentRepo as any);
|
||||
});
|
||||
|
||||
it('returns payment status for owner', async () => {
|
||||
const payment = createPayment();
|
||||
mockPaymentRepo.findById.mockResolvedValue(payment);
|
||||
|
||||
const query = new GetPaymentStatusQuery('pay-1', 'user-1');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.id).toBe('pay-1');
|
||||
expect(result.provider).toBe('VNPAY');
|
||||
expect(result.status).toBe('PROCESSING');
|
||||
expect(result.amountVND).toBe('500000');
|
||||
expect(result.providerTxId).toBe('vnpay-tx-1');
|
||||
});
|
||||
|
||||
it('throws NotFoundException when payment not found', async () => {
|
||||
mockPaymentRepo.findById.mockResolvedValue(null);
|
||||
|
||||
const query = new GetPaymentStatusQuery('nonexistent', 'user-1');
|
||||
|
||||
await expect(handler.execute(query)).rejects.toThrow('Payment');
|
||||
});
|
||||
|
||||
it('throws ForbiddenException when user is not owner', async () => {
|
||||
const payment = createPayment();
|
||||
mockPaymentRepo.findById.mockResolvedValue(payment);
|
||||
|
||||
const query = new GetPaymentStatusQuery('pay-1', 'other-user');
|
||||
|
||||
await expect(handler.execute(query)).rejects.toThrow(/quyền/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { PaymentEntity } from '../../domain/entities/payment.entity';
|
||||
import { type IPaymentRepository } from '../../domain/repositories/payment.repository';
|
||||
import { Money } from '../../domain/value-objects/money.vo';
|
||||
import { HandleCallbackCommand } from '../commands/handle-callback/handle-callback.command';
|
||||
import { HandleCallbackHandler } from '../commands/handle-callback/handle-callback.handler';
|
||||
|
||||
function createPaymentEntity(status: 'PENDING' | 'PROCESSING' | 'COMPLETED' = 'PROCESSING'): PaymentEntity {
|
||||
const money = Money.create(500_000n).unwrap();
|
||||
const payment = PaymentEntity.createNew('pay-1', 'user-1', 'VNPAY', 'SUBSCRIPTION', money);
|
||||
if (status === 'PROCESSING') payment.markProcessing('vnpay-tx-1');
|
||||
if (status === 'COMPLETED') {
|
||||
payment.markProcessing('vnpay-tx-1');
|
||||
payment.markCompleted({ verified: true });
|
||||
}
|
||||
payment.clearDomainEvents();
|
||||
return payment;
|
||||
}
|
||||
|
||||
describe('HandleCallbackHandler', () => {
|
||||
let handler: HandleCallbackHandler;
|
||||
let mockPaymentRepo: { [K in keyof IPaymentRepository]: ReturnType<typeof vi.fn> };
|
||||
let mockGatewayFactory: { getGateway: ReturnType<typeof vi.fn> };
|
||||
let mockGateway: { verifyCallback: ReturnType<typeof vi.fn>; createPaymentUrl: ReturnType<typeof vi.fn>; refund: ReturnType<typeof vi.fn> };
|
||||
let mockEventBus: { publish: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockPaymentRepo = {
|
||||
findById: vi.fn(),
|
||||
findByProviderTxId: vi.fn(),
|
||||
findByIdempotencyKey: vi.fn(),
|
||||
findByUserId: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
updateIfStatus: vi.fn(),
|
||||
};
|
||||
|
||||
mockGateway = {
|
||||
verifyCallback: vi.fn(),
|
||||
createPaymentUrl: vi.fn(),
|
||||
refund: vi.fn(),
|
||||
};
|
||||
|
||||
mockGatewayFactory = { getGateway: vi.fn().mockReturnValue(mockGateway) };
|
||||
mockEventBus = { publish: vi.fn() };
|
||||
|
||||
handler = new HandleCallbackHandler(
|
||||
mockPaymentRepo as any,
|
||||
mockGatewayFactory as any,
|
||||
mockEventBus as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('handles successful callback', async () => {
|
||||
const payment = createPaymentEntity('PROCESSING');
|
||||
mockGateway.verifyCallback.mockReturnValue({
|
||||
isValid: true,
|
||||
orderId: 'pay-1',
|
||||
providerTxId: 'vnpay-tx-1',
|
||||
isSuccess: true,
|
||||
rawData: { responseCode: '00' },
|
||||
});
|
||||
mockPaymentRepo.updateIfStatus.mockResolvedValue(payment);
|
||||
|
||||
const command = new HandleCallbackCommand('VNPAY', { vnp_ResponseCode: '00' });
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.isSuccess).toBe(true);
|
||||
expect(result.paymentId).toBe('pay-1');
|
||||
expect(mockEventBus.publish).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles failed callback', async () => {
|
||||
const payment = createPaymentEntity('PROCESSING');
|
||||
mockGateway.verifyCallback.mockReturnValue({
|
||||
isValid: true,
|
||||
orderId: 'pay-1',
|
||||
providerTxId: 'vnpay-tx-1',
|
||||
isSuccess: false,
|
||||
rawData: { responseCode: '24' },
|
||||
});
|
||||
mockPaymentRepo.updateIfStatus.mockResolvedValue(payment);
|
||||
|
||||
const command = new HandleCallbackCommand('VNPAY', { vnp_ResponseCode: '24' });
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.isSuccess).toBe(false);
|
||||
expect(mockEventBus.publish).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws ValidationException for invalid callback signature', async () => {
|
||||
mockGateway.verifyCallback.mockReturnValue({
|
||||
isValid: false,
|
||||
orderId: '',
|
||||
providerTxId: '',
|
||||
isSuccess: false,
|
||||
rawData: {},
|
||||
});
|
||||
|
||||
const command = new HandleCallbackCommand('VNPAY', { tampered: 'true' });
|
||||
|
||||
await expect(handler.execute(command)).rejects.toThrow(/callback/);
|
||||
});
|
||||
|
||||
it('returns idempotent response for already processed payment', async () => {
|
||||
const completedPayment = createPaymentEntity('COMPLETED');
|
||||
mockGateway.verifyCallback.mockReturnValue({
|
||||
isValid: true,
|
||||
orderId: 'pay-1',
|
||||
providerTxId: 'vnpay-tx-1',
|
||||
isSuccess: true,
|
||||
rawData: {},
|
||||
});
|
||||
mockPaymentRepo.updateIfStatus.mockResolvedValue(null);
|
||||
mockPaymentRepo.findById.mockResolvedValue(completedPayment);
|
||||
|
||||
const command = new HandleCallbackCommand('VNPAY', { vnp_ResponseCode: '00' });
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.paymentId).toBe('pay-1');
|
||||
expect(result.status).toBe('COMPLETED');
|
||||
expect(result.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
it('throws NotFoundException when payment not found after failed update', async () => {
|
||||
mockGateway.verifyCallback.mockReturnValue({
|
||||
isValid: true,
|
||||
orderId: 'nonexistent',
|
||||
providerTxId: 'tx-1',
|
||||
isSuccess: true,
|
||||
rawData: {},
|
||||
});
|
||||
mockPaymentRepo.updateIfStatus.mockResolvedValue(null);
|
||||
mockPaymentRepo.findById.mockResolvedValue(null);
|
||||
|
||||
const command = new HandleCallbackCommand('VNPAY', { vnp_ResponseCode: '00' });
|
||||
|
||||
await expect(handler.execute(command)).rejects.toThrow('Payment');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { PaymentEntity } from '../../domain/entities/payment.entity';
|
||||
import { type IPaymentRepository } from '../../domain/repositories/payment.repository';
|
||||
import { Money } from '../../domain/value-objects/money.vo';
|
||||
import { ListTransactionsQuery } from '../queries/list-transactions/list-transactions.query';
|
||||
import { ListTransactionsHandler } from '../queries/list-transactions/list-transactions.handler';
|
||||
|
||||
function createPayment(id: string, status: 'PENDING' | 'COMPLETED' = 'COMPLETED'): PaymentEntity {
|
||||
const money = Money.create(1_000_000n).unwrap();
|
||||
const payment = PaymentEntity.createNew(id, 'user-1', 'VNPAY', 'SUBSCRIPTION', money);
|
||||
if (status === 'COMPLETED') {
|
||||
payment.markProcessing('tx-' + id);
|
||||
payment.markCompleted({ verified: true });
|
||||
}
|
||||
payment.clearDomainEvents();
|
||||
return payment;
|
||||
}
|
||||
|
||||
describe('ListTransactionsHandler', () => {
|
||||
let handler: ListTransactionsHandler;
|
||||
let mockPaymentRepo: { [K in keyof IPaymentRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockPaymentRepo = {
|
||||
findById: vi.fn(),
|
||||
findByProviderTxId: vi.fn(),
|
||||
findByIdempotencyKey: vi.fn(),
|
||||
findByUserId: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
updateIfStatus: vi.fn(),
|
||||
};
|
||||
|
||||
handler = new ListTransactionsHandler(mockPaymentRepo as any);
|
||||
});
|
||||
|
||||
it('returns paginated transactions', async () => {
|
||||
const payments = [createPayment('pay-1'), createPayment('pay-2')];
|
||||
mockPaymentRepo.findByUserId.mockResolvedValue({ items: payments, total: 2 });
|
||||
|
||||
const query = new ListTransactionsQuery('user-1');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.items).toHaveLength(2);
|
||||
expect(result.total).toBe(2);
|
||||
expect(result.items[0].amountVND).toBe('1000000');
|
||||
expect(result.limit).toBe(20);
|
||||
expect(result.offset).toBe(0);
|
||||
});
|
||||
|
||||
it('applies custom limit and offset', async () => {
|
||||
mockPaymentRepo.findByUserId.mockResolvedValue({ items: [], total: 0 });
|
||||
|
||||
const query = new ListTransactionsQuery('user-1', undefined, 10, 20);
|
||||
await handler.execute(query);
|
||||
|
||||
expect(mockPaymentRepo.findByUserId).toHaveBeenCalledWith('user-1', {
|
||||
status: undefined,
|
||||
limit: 10,
|
||||
offset: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it('caps limit at 100', async () => {
|
||||
mockPaymentRepo.findByUserId.mockResolvedValue({ items: [], total: 0 });
|
||||
|
||||
const query = new ListTransactionsQuery('user-1', undefined, 500);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.limit).toBe(100);
|
||||
});
|
||||
|
||||
it('filters by status', async () => {
|
||||
mockPaymentRepo.findByUserId.mockResolvedValue({ items: [], total: 0 });
|
||||
|
||||
const query = new ListTransactionsQuery('user-1', 'COMPLETED');
|
||||
await handler.execute(query);
|
||||
|
||||
expect(mockPaymentRepo.findByUserId).toHaveBeenCalledWith('user-1',
|
||||
expect.objectContaining({ status: 'COMPLETED' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { PaymentEntity } from '../../domain/entities/payment.entity';
|
||||
import { type IPaymentRepository } from '../../domain/repositories/payment.repository';
|
||||
import { Money } from '../../domain/value-objects/money.vo';
|
||||
import { RefundPaymentCommand } from '../commands/refund-payment/refund-payment.command';
|
||||
import { RefundPaymentHandler } from '../commands/refund-payment/refund-payment.handler';
|
||||
|
||||
function createCompletedPayment(): PaymentEntity {
|
||||
const money = Money.create(1_000_000n).unwrap();
|
||||
const payment = PaymentEntity.createNew('pay-1', 'user-1', 'VNPAY', 'SUBSCRIPTION', money);
|
||||
payment.markProcessing('vnpay-tx-1');
|
||||
payment.markCompleted({ verified: true });
|
||||
payment.clearDomainEvents();
|
||||
return payment;
|
||||
}
|
||||
|
||||
describe('RefundPaymentHandler', () => {
|
||||
let handler: RefundPaymentHandler;
|
||||
let mockPaymentRepo: { [K in keyof IPaymentRepository]: ReturnType<typeof vi.fn> };
|
||||
let mockGatewayFactory: { getGateway: ReturnType<typeof vi.fn> };
|
||||
let mockGateway: { refund: ReturnType<typeof vi.fn>; createPaymentUrl: ReturnType<typeof vi.fn>; verifyCallback: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockPaymentRepo = {
|
||||
findById: vi.fn(),
|
||||
findByProviderTxId: vi.fn(),
|
||||
findByIdempotencyKey: vi.fn(),
|
||||
findByUserId: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
updateIfStatus: vi.fn(),
|
||||
};
|
||||
|
||||
mockGateway = {
|
||||
refund: vi.fn(),
|
||||
createPaymentUrl: vi.fn(),
|
||||
verifyCallback: vi.fn(),
|
||||
};
|
||||
|
||||
mockGatewayFactory = { getGateway: vi.fn().mockReturnValue(mockGateway) };
|
||||
|
||||
handler = new RefundPaymentHandler(
|
||||
mockPaymentRepo as any,
|
||||
mockGatewayFactory as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('refunds a completed payment successfully', async () => {
|
||||
const payment = createCompletedPayment();
|
||||
mockPaymentRepo.findById.mockResolvedValue(payment);
|
||||
mockGateway.refund.mockResolvedValue({ success: true, refundTxId: 'refund-tx-1' });
|
||||
|
||||
const command = new RefundPaymentCommand('pay-1', 'Yêu cầu hoàn tiền', 'admin-1');
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.refundTxId).toBe('refund-tx-1');
|
||||
expect(result.paymentId).toBe('pay-1');
|
||||
expect(mockPaymentRepo.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles failed refund from gateway', async () => {
|
||||
const payment = createCompletedPayment();
|
||||
mockPaymentRepo.findById.mockResolvedValue(payment);
|
||||
mockGateway.refund.mockResolvedValue({ success: false, refundTxId: null });
|
||||
|
||||
const command = new RefundPaymentCommand('pay-1', 'Hoàn tiền', 'admin-1');
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(mockPaymentRepo.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws NotFoundException when payment not found', async () => {
|
||||
mockPaymentRepo.findById.mockResolvedValue(null);
|
||||
|
||||
const command = new RefundPaymentCommand('nonexistent', 'reason', 'admin-1');
|
||||
|
||||
await expect(handler.execute(command)).rejects.toThrow('Payment');
|
||||
});
|
||||
|
||||
it('throws ValidationException when payment is not completed', async () => {
|
||||
const money = Money.create(500_000n).unwrap();
|
||||
const payment = PaymentEntity.createNew('pay-2', 'user-1', 'VNPAY', 'SUBSCRIPTION', money);
|
||||
payment.clearDomainEvents();
|
||||
mockPaymentRepo.findById.mockResolvedValue(payment);
|
||||
|
||||
const command = new RefundPaymentCommand('pay-2', 'reason', 'admin-1');
|
||||
|
||||
await expect(handler.execute(command)).rejects.toThrow(/hoàn tất/);
|
||||
});
|
||||
|
||||
it('throws ValidationException when no provider transaction id', async () => {
|
||||
const money = Money.create(500_000n).unwrap();
|
||||
const payment = PaymentEntity.createNew('pay-3', 'user-1', 'VNPAY', 'SUBSCRIPTION', money);
|
||||
// Manually mark completed without providerTxId by using internal hack
|
||||
(payment as any)._status = 'COMPLETED';
|
||||
(payment as any)._providerTxId = null;
|
||||
payment.clearDomainEvents();
|
||||
mockPaymentRepo.findById.mockResolvedValue(payment);
|
||||
|
||||
const command = new RefundPaymentCommand('pay-3', 'reason', 'admin-1');
|
||||
|
||||
await expect(handler.execute(command)).rejects.toThrow(/mã giao dịch/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user