Skip to content

Commit 8fd5217

Browse files
committed
test(billing): add unit tests for createPaymentIntent function
- 9 new tests with Stripe API mocking via mockModule - Tests correct parameter passing (amount, customer, payment method, description) - Verifies currency=usd, off_session=true, confirm=true settings - Tests idempotency key passing for safe retries - Tests metadata passing and error propagation - Total: 51 tests for auto-topup-helpers (was 42, now 51)
1 parent 8e5b789 commit 8fd5217

File tree

1 file changed

+223
-0
lines changed

1 file changed

+223
-0
lines changed

packages/billing/src/__tests__/auto-topup-helpers.test.ts

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
66

77
import {
8+
createPaymentIntent,
89
fetchPaymentMethods,
910
filterValidPaymentMethods,
1011
findValidPaymentMethod,
@@ -233,6 +234,228 @@ describe('auto-topup-helpers', () => {
233234
})
234235
})
235236

237+
describe('createPaymentIntent', () => {
238+
let mockPaymentIntentsCreate: {
239+
calls: Array<{ params: any; options: any }>
240+
create: (params: any, options?: any) => Promise<any>
241+
mockResponse: any
242+
mockError: Error | null
243+
}
244+
245+
function createMockPaymentIntentsCreate(options?: {
246+
response?: any
247+
error?: Error
248+
}) {
249+
const calls: Array<{ params: any; options: any }> = []
250+
const mockResponse = options?.response ?? {
251+
id: 'pi_test_123',
252+
status: 'succeeded',
253+
amount: 1000,
254+
currency: 'usd',
255+
}
256+
const mockError = options?.error ?? null
257+
258+
return {
259+
calls,
260+
mockResponse,
261+
mockError,
262+
create: async (params: any, opts?: any) => {
263+
calls.push({ params, options: opts })
264+
if (mockError) {
265+
throw mockError
266+
}
267+
return mockResponse
268+
},
269+
}
270+
}
271+
272+
beforeEach(async () => {
273+
mockPaymentIntentsCreate = createMockPaymentIntentsCreate()
274+
await mockModule('@codebuff/internal/util/stripe', () => ({
275+
stripeServer: {
276+
paymentIntents: mockPaymentIntentsCreate,
277+
},
278+
}))
279+
})
280+
281+
afterEach(() => {
282+
clearMockedModules()
283+
})
284+
285+
it('should create a payment intent with correct parameters', async () => {
286+
const params = {
287+
amountInCents: 5000,
288+
stripeCustomerId: 'cus_123',
289+
paymentMethodId: 'pm_card_123',
290+
description: 'Auto top-up for user',
291+
idempotencyKey: 'idem_key_123',
292+
metadata: { userId: 'user_123', type: 'auto_topup' },
293+
}
294+
295+
await createPaymentIntent(params)
296+
297+
expect(mockPaymentIntentsCreate.calls).toHaveLength(1)
298+
const call = mockPaymentIntentsCreate.calls[0]
299+
300+
expect(call.params).toEqual({
301+
amount: 5000,
302+
currency: 'usd',
303+
customer: 'cus_123',
304+
payment_method: 'pm_card_123',
305+
off_session: true,
306+
confirm: true,
307+
description: 'Auto top-up for user',
308+
metadata: { userId: 'user_123', type: 'auto_topup' },
309+
})
310+
expect(call.options).toEqual({ idempotencyKey: 'idem_key_123' })
311+
})
312+
313+
it('should return the payment intent from Stripe', async () => {
314+
const expectedResponse = {
315+
id: 'pi_custom_123',
316+
status: 'succeeded',
317+
amount: 10000,
318+
currency: 'usd',
319+
customer: 'cus_456',
320+
} as Stripe.PaymentIntent
321+
322+
mockPaymentIntentsCreate = createMockPaymentIntentsCreate({
323+
response: expectedResponse,
324+
})
325+
await mockModule('@codebuff/internal/util/stripe', () => ({
326+
stripeServer: {
327+
paymentIntents: mockPaymentIntentsCreate,
328+
},
329+
}))
330+
331+
const result = await createPaymentIntent({
332+
amountInCents: 10000,
333+
stripeCustomerId: 'cus_456',
334+
paymentMethodId: 'pm_card_456',
335+
description: 'Test payment',
336+
idempotencyKey: 'idem_456',
337+
metadata: {},
338+
})
339+
340+
expect(result.id).toBe('pi_custom_123')
341+
expect(result.status).toBe('succeeded')
342+
expect(result.amount).toBe(10000)
343+
})
344+
345+
it('should always set currency to usd', async () => {
346+
await createPaymentIntent({
347+
amountInCents: 1000,
348+
stripeCustomerId: 'cus_test',
349+
paymentMethodId: 'pm_test',
350+
description: 'Test',
351+
idempotencyKey: 'idem_test',
352+
metadata: {},
353+
})
354+
355+
expect(mockPaymentIntentsCreate.calls[0].params.currency).toBe('usd')
356+
})
357+
358+
it('should always set off_session to true for auto-topup', async () => {
359+
await createPaymentIntent({
360+
amountInCents: 1000,
361+
stripeCustomerId: 'cus_test',
362+
paymentMethodId: 'pm_test',
363+
description: 'Test',
364+
idempotencyKey: 'idem_test',
365+
metadata: {},
366+
})
367+
368+
expect(mockPaymentIntentsCreate.calls[0].params.off_session).toBe(true)
369+
})
370+
371+
it('should always set confirm to true to immediately charge', async () => {
372+
await createPaymentIntent({
373+
amountInCents: 1000,
374+
stripeCustomerId: 'cus_test',
375+
paymentMethodId: 'pm_test',
376+
description: 'Test',
377+
idempotencyKey: 'idem_test',
378+
metadata: {},
379+
})
380+
381+
expect(mockPaymentIntentsCreate.calls[0].params.confirm).toBe(true)
382+
})
383+
384+
it('should pass idempotency key in options for safe retries', async () => {
385+
const idempotencyKey = 'unique_idem_key_789'
386+
387+
await createPaymentIntent({
388+
amountInCents: 1000,
389+
stripeCustomerId: 'cus_test',
390+
paymentMethodId: 'pm_test',
391+
description: 'Test',
392+
idempotencyKey,
393+
metadata: {},
394+
})
395+
396+
expect(mockPaymentIntentsCreate.calls[0].options.idempotencyKey).toBe(
397+
idempotencyKey,
398+
)
399+
})
400+
401+
it('should pass metadata to Stripe', async () => {
402+
const metadata = {
403+
userId: 'user_123',
404+
organizationId: 'org_456',
405+
type: 'auto_topup',
406+
trigger: 'low_balance',
407+
}
408+
409+
await createPaymentIntent({
410+
amountInCents: 1000,
411+
stripeCustomerId: 'cus_test',
412+
paymentMethodId: 'pm_test',
413+
description: 'Test',
414+
idempotencyKey: 'idem_test',
415+
metadata,
416+
})
417+
418+
expect(mockPaymentIntentsCreate.calls[0].params.metadata).toEqual(metadata)
419+
})
420+
421+
it('should propagate Stripe errors', async () => {
422+
const stripeError = new Error('Card declined')
423+
424+
mockPaymentIntentsCreate = createMockPaymentIntentsCreate({
425+
error: stripeError,
426+
})
427+
await mockModule('@codebuff/internal/util/stripe', () => ({
428+
stripeServer: {
429+
paymentIntents: mockPaymentIntentsCreate,
430+
},
431+
}))
432+
433+
await expect(
434+
createPaymentIntent({
435+
amountInCents: 1000,
436+
stripeCustomerId: 'cus_test',
437+
paymentMethodId: 'pm_declined',
438+
description: 'Test',
439+
idempotencyKey: 'idem_test',
440+
metadata: {},
441+
}),
442+
).rejects.toThrow('Card declined')
443+
})
444+
445+
it('should handle empty metadata', async () => {
446+
await createPaymentIntent({
447+
amountInCents: 1000,
448+
stripeCustomerId: 'cus_test',
449+
paymentMethodId: 'pm_test',
450+
description: 'Test',
451+
idempotencyKey: 'idem_test',
452+
metadata: {},
453+
})
454+
455+
expect(mockPaymentIntentsCreate.calls[0].params.metadata).toEqual({})
456+
})
457+
})
458+
236459
describe('isValidPaymentMethod', () => {
237460
describe('card payment methods', () => {
238461
it('should return true for card with future expiration date', () => {

0 commit comments

Comments
 (0)