feat: add yookassa API client library with tests
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createPayment, getPayment } from '../yookassa.js'
|
||||
|
||||
describe('yookassa createPayment', () => {
|
||||
beforeEach(() => {
|
||||
process.env.YOOKASSA_SHOP_ID = '123456'
|
||||
process.env.YOOKASSA_SECRET_KEY = 'test_secret'
|
||||
vi.stubGlobal('fetch', vi.fn())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
delete process.env.YOOKASSA_SHOP_ID
|
||||
delete process.env.YOOKASSA_SECRET_KEY
|
||||
})
|
||||
|
||||
it('calls POST /payments with Basic auth and Idempotence-Key', async () => {
|
||||
const mockPayment = {
|
||||
id: '2d0c6f35-000f-5000-8000-1234567890ab',
|
||||
status: 'pending',
|
||||
paid: false,
|
||||
amount: { value: '1000.00', currency: 'RUB' },
|
||||
confirmation: { type: 'redirect', confirmation_url: 'https://yoomoney.ru/checkout/...' },
|
||||
created_at: '2026-05-20T12:00:00.000Z',
|
||||
test: true,
|
||||
refundable: false,
|
||||
recipient: { account_id: '123456', gateway_id: '123456' },
|
||||
}
|
||||
fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve(mockPayment),
|
||||
})
|
||||
|
||||
const result = await createPayment({
|
||||
amount: { value: '1000.00', currency: 'RUB' },
|
||||
description: 'Order #test',
|
||||
receipt: {
|
||||
customer: { email: 'test@example.com' },
|
||||
items: [{ description: 'Item', quantity: 1, amount: { value: '1000.00', currency: 'RUB' }, vat_code: 1 }],
|
||||
tax_system_code: 1,
|
||||
},
|
||||
confirmation: { type: 'redirect', return_url: 'http://localhost:5173/me/orders/test?paid=1' },
|
||||
metadata: { orderId: 'test' },
|
||||
idempotencyKey: 'test-v1',
|
||||
})
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1)
|
||||
const [url, opts] = fetch.mock.calls[0]
|
||||
expect(url).toBe('https://api.yookassa.ru/v3/payments')
|
||||
expect(opts.method).toBe('POST')
|
||||
expect(opts.headers['Idempotence-Key']).toBe('test-v1')
|
||||
expect(opts.headers['Authorization']).toBe('Basic MTIzNDU2OnRlc3Rfc2VjcmV0')
|
||||
expect(result.paymentId).toBe('2d0c6f35-000f-5000-8000-1234567890ab')
|
||||
expect(result.confirmationUrl).toBe('https://yoomoney.ru/checkout/...')
|
||||
expect(result.status).toBe('pending')
|
||||
})
|
||||
|
||||
it('retries on 5xx error', async () => {
|
||||
fetch
|
||||
.mockResolvedValueOnce({ ok: false, status: 500 })
|
||||
.mockResolvedValueOnce({ ok: false, status: 503 })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'retry-id',
|
||||
status: 'pending',
|
||||
paid: false,
|
||||
amount: { value: '500.00', currency: 'RUB' },
|
||||
confirmation: { type: 'redirect', confirmation_url: 'https://yoomoney.ru/checkout/retry' },
|
||||
created_at: '2026-05-20T12:00:00.000Z',
|
||||
test: true,
|
||||
refundable: false,
|
||||
recipient: { account_id: '123456', gateway_id: '123456' },
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await createPayment({
|
||||
amount: { value: '500.00', currency: 'RUB' },
|
||||
description: 'Retry test',
|
||||
receipt: {
|
||||
customer: { email: 'test@example.com' },
|
||||
items: [{ description: 'Item', quantity: 1, amount: { value: '500.00', currency: 'RUB' }, vat_code: 1 }],
|
||||
tax_system_code: 1,
|
||||
},
|
||||
confirmation: { type: 'redirect', return_url: 'http://localhost:5173/me/orders/test' },
|
||||
metadata: {},
|
||||
idempotencyKey: 'retry-v1',
|
||||
})
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(3)
|
||||
expect(result.paymentId).toBe('retry-id')
|
||||
})
|
||||
|
||||
it('throws on 4xx error', async () => {
|
||||
fetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
type: 'error',
|
||||
id: 'err-id',
|
||||
code: 'invalid_request',
|
||||
description: 'Missing required field',
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(
|
||||
createPayment({
|
||||
amount: { value: '1000.00', currency: 'RUB' },
|
||||
description: 'Bad request',
|
||||
receipt: {
|
||||
customer: { email: 'test@example.com' },
|
||||
items: [{ description: 'Item', quantity: 1, amount: { value: '1000.00', currency: 'RUB' }, vat_code: 1 }],
|
||||
tax_system_code: 1,
|
||||
},
|
||||
confirmation: { type: 'redirect', return_url: 'http://localhost:5173' },
|
||||
metadata: {},
|
||||
idempotencyKey: 'bad-v1',
|
||||
}),
|
||||
).rejects.toThrow('YooKassa API error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('yookassa getPayment', () => {
|
||||
beforeEach(() => {
|
||||
process.env.YOOKASSA_SHOP_ID = '123456'
|
||||
process.env.YOOKASSA_SECRET_KEY = 'test_secret'
|
||||
vi.stubGlobal('fetch', vi.fn())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
delete process.env.YOOKASSA_SHOP_ID
|
||||
delete process.env.YOOKASSA_SECRET_KEY
|
||||
})
|
||||
|
||||
it('calls GET /payments/{id} and returns payment data', async () => {
|
||||
fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'payment-id',
|
||||
status: 'succeeded',
|
||||
paid: true,
|
||||
amount: { value: '1000.00', currency: 'RUB' },
|
||||
created_at: '2026-05-20T12:00:00.000Z',
|
||||
test: true,
|
||||
refundable: true,
|
||||
recipient: { account_id: '123456', gateway_id: '123456' },
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await getPayment('payment-id')
|
||||
expect(fetch).toHaveBeenCalledTimes(1)
|
||||
expect(fetch.mock.calls[0][0]).toBe('https://api.yookassa.ru/v3/payments/payment-id')
|
||||
expect(result.id).toBe('payment-id')
|
||||
expect(result.status).toBe('succeeded')
|
||||
expect(result.paid).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user