ghostguild-org/tests/server/api/update-contribution.test.js

207 lines
6.8 KiB
JavaScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import Member from '../../../server/models/member.js'
import {
requiresPayment,
getContributionTierByValue,
getTierAmount,
} from '../../../server/config/contributions.js'
import { updateHelcimSubscription } from '../../../server/utils/helcim.js'
import handler from '../../../server/api/members/update-contribution.post.js'
import { createMockEvent } from '../helpers/createMockEvent.js'
vi.mock('../../../server/models/member.js', () => ({
default: { findByIdAndUpdate: vi.fn() }
}))
vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() }))
vi.mock('../../../server/utils/auth.js', () => ({ requireAuth: vi.fn() }))
vi.mock('../../../server/config/contributions.js', () => ({
requiresPayment: vi.fn(),
getHelcimPlanId: vi.fn(),
getContributionTierByValue: vi.fn(),
getTierAmount: vi.fn(),
}))
vi.mock('../../../server/utils/helcim.js', () => ({
getHelcimCustomer: vi.fn(),
listHelcimCustomerCards: vi.fn(),
createHelcimSubscription: vi.fn(),
updateHelcimSubscription: vi.fn(),
cancelHelcimSubscription: vi.fn(),
generateIdempotencyKey: vi.fn().mockReturnValue('idem-key-123'),
}))
// Nitro auto-imports
vi.stubGlobal('updateContributionSchema', {})
describe('update-contribution endpoint — Case 3 (paid→paid)', () => {
beforeEach(() => {
vi.clearAllMocks()
// Both tiers require payment for all Case 3 tests
requiresPayment.mockReturnValue(true)
})
// Helper: set requireAuth global to resolve with the given member
function setMember(mockMember) {
globalThis.requireAuth = vi.fn().mockResolvedValue(mockMember)
}
it('monthly $5 → $15: calls updateHelcimSubscription with recurringAmount and updates member', async () => {
const mockMember = {
_id: 'member-1',
contributionTier: '5',
helcimSubscriptionId: 'sub-1',
billingCadence: 'monthly',
}
setMember(mockMember)
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
getTierAmount.mockReturnValue(15)
updateHelcimSubscription.mockResolvedValue({ id: 'sub-1', status: 'active' })
Member.findByIdAndUpdate.mockResolvedValue({})
const event = createMockEvent({
method: 'POST',
path: '/api/members/update-contribution',
body: { contributionTier: '15' },
})
const result = await handler(event)
expect(updateHelcimSubscription).toHaveBeenCalledWith('sub-1', { recurringAmount: 15 })
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
'member-1',
{ $set: { contributionTier: '15' } },
{ runValidators: false }
)
expect(result.success).toBe(true)
expect(result.message).toBe('Successfully updated contribution level')
})
it('annual $5 → $15: calls updateHelcimSubscription with recurringAmount 150', async () => {
const mockMember = {
_id: 'member-2',
contributionTier: '5',
helcimSubscriptionId: 'sub-2',
billingCadence: 'annual',
}
setMember(mockMember)
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
getTierAmount.mockReturnValue(150)
updateHelcimSubscription.mockResolvedValue({ id: 'sub-2', status: 'active' })
Member.findByIdAndUpdate.mockResolvedValue({})
const event = createMockEvent({
method: 'POST',
path: '/api/members/update-contribution',
body: { contributionTier: '15', cadence: 'annual' },
})
const result = await handler(event)
expect(updateHelcimSubscription).toHaveBeenCalledWith('sub-2', { recurringAmount: 150 })
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
'member-2',
{ $set: { contributionTier: '15' } },
{ runValidators: false }
)
expect(result.success).toBe(true)
})
it('annual $15 → $50: calls updateHelcimSubscription with recurringAmount 500', async () => {
const mockMember = {
_id: 'member-3',
contributionTier: '15',
helcimSubscriptionId: 'sub-3',
billingCadence: 'annual',
}
setMember(mockMember)
getContributionTierByValue.mockReturnValue({ value: '50', amount: '50' })
getTierAmount.mockReturnValue(500)
updateHelcimSubscription.mockResolvedValue({ id: 'sub-3', status: 'active' })
Member.findByIdAndUpdate.mockResolvedValue({})
const event = createMockEvent({
method: 'POST',
path: '/api/members/update-contribution',
body: { contributionTier: '50', cadence: 'annual' },
})
await handler(event)
expect(updateHelcimSubscription).toHaveBeenCalledWith('sub-3', { recurringAmount: 500 })
})
it('cadence mismatch: monthly member + body cadence annual → 400, no Helcim call, no DB write', async () => {
const mockMember = {
_id: 'member-4',
contributionTier: '5',
helcimSubscriptionId: 'sub-4',
billingCadence: 'monthly',
}
setMember(mockMember)
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
const event = createMockEvent({
method: 'POST',
path: '/api/members/update-contribution',
body: { contributionTier: '15', cadence: 'annual' },
})
await expect(handler(event)).rejects.toMatchObject({
statusCode: 400,
statusMessage: 'Cadence switch not supported on existing subscription',
})
expect(updateHelcimSubscription).not.toHaveBeenCalled()
expect(Member.findByIdAndUpdate).not.toHaveBeenCalled()
})
it('Helcim PATCH failure → 500, member NOT updated', async () => {
const mockMember = {
_id: 'member-5',
contributionTier: '5',
helcimSubscriptionId: 'sub-5',
billingCadence: 'monthly',
}
setMember(mockMember)
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
getTierAmount.mockReturnValue(15)
updateHelcimSubscription.mockRejectedValue(new Error('Helcim 400'))
const event = createMockEvent({
method: 'POST',
path: '/api/members/update-contribution',
body: { contributionTier: '15' },
})
await expect(handler(event)).rejects.toMatchObject({
statusCode: 500,
statusMessage: 'Subscription update failed',
})
expect(Member.findByIdAndUpdate).not.toHaveBeenCalled()
})
it('no helcimSubscriptionId → 400 with requiresPaymentSetup, no Helcim call', async () => {
const mockMember = {
_id: 'member-6',
contributionTier: '5',
helcimSubscriptionId: null,
billingCadence: 'monthly',
}
setMember(mockMember)
const event = createMockEvent({
method: 'POST',
path: '/api/members/update-contribution',
body: { contributionTier: '15' },
})
await expect(handler(event)).rejects.toMatchObject({
statusCode: 400,
data: { requiresPaymentSetup: true },
})
expect(updateHelcimSubscription).not.toHaveBeenCalled()
expect(Member.findByIdAndUpdate).not.toHaveBeenCalled()
})
})