From 613d077eaad70c1f66dc6703029658285e9ae07e Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sun, 19 Apr 2026 18:35:25 +0100 Subject: [PATCH] =?UTF-8?q?feat(helcim):=20use=20contributionAmount,=20inl?= =?UTF-8?q?ine=20=C3=9712=20annual=20math?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/helcim/subscription.post.js | 30 ++++++----- tests/server/api/helcim-subscription.test.js | 56 ++++++++------------ 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/server/api/helcim/subscription.post.js b/server/api/helcim/subscription.post.js index 7d9c015..c939391 100644 --- a/server/api/helcim/subscription.post.js +++ b/server/api/helcim/subscription.post.js @@ -1,5 +1,5 @@ // Create a Helcim subscription -import { getHelcimPlanId, requiresPayment, getContributionTierByValue, getTierAmount } from '../../config/contributions.js' +import { getHelcimPlanId, requiresPayment } from '../../config/contributions.js' import Member from '../../models/member.js' import { connectDB } from '../../utils/mongoose.js' import { getSlackService } from '../../utils/slack.ts' @@ -46,7 +46,7 @@ async function inviteToSlack(member) { member.name, member.email, member.circle, - member.contributionTier, + member.contributionAmount, inviteResult.status ) @@ -93,19 +93,19 @@ export default defineEventHandler(async (event) => { const isFirstActivation = priorMember?.status === 'pending_payment' // Check if payment is required - if (!requiresPayment(body.contributionTier)) { + if (!requiresPayment(body.contributionAmount)) { // For free tier, just update member status const member = await Member.findOneAndUpdate( { helcimCustomerId: body.customerId }, - { + { status: 'active', - contributionTier: body.contributionTier, + contributionAmount: body.contributionAmount, subscriptionStartDate: new Date() }, { new: true } ) - - logActivity(member._id, 'subscription_created', { tier: body.contributionTier }) + + logActivity(member._id, 'subscription_created', { amount: body.contributionAmount }) await inviteToSlack(member) if (isFirstActivation) await sendWelcomeEmail(member) @@ -118,7 +118,7 @@ export default defineEventHandler(async (event) => { email: member.email, name: member.name, circle: member.circle, - contributionTier: member.contributionTier, + contributionAmount: member.contributionAmount, status: member.status } } @@ -128,13 +128,15 @@ export default defineEventHandler(async (event) => { if (!body.cardToken) { throw createError({ statusCode: 400, - statusMessage: 'Payment information is required for this contribution tier' + statusMessage: 'Payment information is required for a paid contribution' }) } - const tierInfo = getContributionTierByValue(body.contributionTier) const cadence = body.cadence const paymentPlanId = getHelcimPlanId(cadence) + const recurringAmount = cadence === 'annual' + ? body.contributionAmount * 12 + : body.contributionAmount if (!paymentPlanId) { throw createError({ @@ -151,7 +153,7 @@ export default defineEventHandler(async (event) => { dateActivated: new Date().toISOString().split('T')[0], paymentPlanId: parseInt(paymentPlanId), customerCode: body.customerCode, - recurringAmount: getTierAmount(tierInfo, cadence), + recurringAmount, paymentMethod: 'card', } @@ -167,7 +169,7 @@ export default defineEventHandler(async (event) => { const member = await Member.findOneAndUpdate( { helcimCustomerId: body.customerId }, { $set: { - contributionTier: body.contributionTier, + contributionAmount: body.contributionAmount, helcimSubscriptionId: subscription.id, helcimCustomerId: body.customerId, paymentMethod: 'card', @@ -178,7 +180,7 @@ export default defineEventHandler(async (event) => { { new: true, runValidators: false } ) - logActivity(member._id, 'subscription_created', { tier: body.contributionTier }) + logActivity(member._id, 'subscription_created', { amount: body.contributionAmount }) await inviteToSlack(member) if (isFirstActivation) await sendWelcomeEmail(member) @@ -195,7 +197,7 @@ export default defineEventHandler(async (event) => { email: member.email, name: member.name, circle: member.circle, - contributionTier: member.contributionTier, + contributionAmount: member.contributionAmount, status: member.status } } diff --git a/tests/server/api/helcim-subscription.test.js b/tests/server/api/helcim-subscription.test.js index c41b881..2fae9d3 100644 --- a/tests/server/api/helcim-subscription.test.js +++ b/tests/server/api/helcim-subscription.test.js @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import Member from '../../../server/models/member.js' import { requireAuth } from '../../../server/utils/auth.js' -import { requiresPayment, getHelcimPlanId, getContributionTierByValue, getTierAmount } from '../../../server/config/contributions.js' +import { requiresPayment, getHelcimPlanId } from '../../../server/config/contributions.js' import { createHelcimSubscription } from '../../../server/utils/helcim.js' import subscriptionHandler from '../../../server/api/helcim/subscription.post.js' import { createMockEvent } from '../helpers/createMockEvent.js' @@ -18,8 +18,6 @@ vi.mock('../../../server/utils/slack.ts', () => ({ vi.mock('../../../server/config/contributions.js', () => ({ requiresPayment: vi.fn(), getHelcimPlanId: vi.fn(), - getContributionTierByValue: vi.fn(), - getTierAmount: vi.fn(), })) vi.mock('../../../server/utils/resend.js', () => ({ sendWelcomeEmail: vi.fn().mockResolvedValue({ success: true }) @@ -47,7 +45,7 @@ describe('helcim subscription endpoint', () => { const event = createMockEvent({ method: 'POST', path: '/api/helcim/subscription', - body: { customerId: 'cust-1', contributionTier: '0', customerCode: 'code-1' } + body: { customerId: 'cust-1', contributionAmount: 0, customerCode: 'code-1' } }) await expect(subscriptionHandler(event)).rejects.toMatchObject({ @@ -67,7 +65,7 @@ describe('helcim subscription endpoint', () => { email: 'test@example.com', name: 'Test', circle: 'community', - contributionTier: '0', + contributionAmount: 0, status: 'active', save: vi.fn() } @@ -76,7 +74,7 @@ describe('helcim subscription endpoint', () => { const event = createMockEvent({ method: 'POST', path: '/api/helcim/subscription', - body: { customerId: 'cust-1', contributionTier: '0', customerCode: 'code-1' } + body: { customerId: 'cust-1', contributionAmount: 0, customerCode: 'code-1' } }) const result = await subscriptionHandler(event) @@ -88,12 +86,12 @@ describe('helcim subscription endpoint', () => { email: 'test@example.com', name: 'Test', circle: 'community', - contributionTier: '0', + contributionAmount: 0, status: 'active' }) expect(Member.findOneAndUpdate).toHaveBeenCalledWith( { helcimCustomerId: 'cust-1' }, - expect.objectContaining({ status: 'active', contributionTier: '0' }), + expect.objectContaining({ status: 'active', contributionAmount: 0 }), { new: true } ) expect(createHelcimSubscription).not.toHaveBeenCalled() @@ -107,12 +105,12 @@ describe('helcim subscription endpoint', () => { const event = createMockEvent({ method: 'POST', path: '/api/helcim/subscription', - body: { customerId: 'cust-1', contributionTier: '15', customerCode: 'code-1', cadence: 'monthly' } + body: { customerId: 'cust-1', contributionAmount: 15, customerCode: 'code-1', cadence: 'monthly' } }) await expect(subscriptionHandler(event)).rejects.toMatchObject({ statusCode: 400, - statusMessage: 'Payment information is required for this contribution tier' + statusMessage: 'Payment information is required for a paid contribution' }) }) @@ -120,15 +118,13 @@ describe('helcim subscription endpoint', () => { requireAuth.mockResolvedValue(undefined) requiresPayment.mockReturnValue(true) getHelcimPlanId.mockReturnValue('99999') - getContributionTierByValue.mockReturnValue({ amount: '15' }) - getTierAmount.mockReturnValue(15) const mockMember = { _id: 'member-2', email: 'paid@example.com', name: 'Paid User', circle: 'founder', - contributionTier: '15', + contributionAmount: 15, status: 'active', } Member.findOneAndUpdate.mockResolvedValue(mockMember) @@ -139,7 +135,7 @@ describe('helcim subscription endpoint', () => { const event = createMockEvent({ method: 'POST', path: '/api/helcim/subscription', - body: { customerId: 'cust-1', contributionTier: '15', customerCode: 'code-1', cardToken: 'tok-123', cadence: 'monthly' } + body: { customerId: 'cust-1', contributionAmount: 15, customerCode: 'code-1', cardToken: 'tok-123', cadence: 'monthly' } }) const result = await subscriptionHandler(event) @@ -151,7 +147,7 @@ describe('helcim subscription endpoint', () => { ) expect(Member.findOneAndUpdate).toHaveBeenCalledWith( { helcimCustomerId: 'cust-1' }, - { $set: expect.objectContaining({ billingCadence: 'monthly', contributionTier: '15', status: 'active' }) }, + { $set: expect.objectContaining({ billingCadence: 'monthly', contributionAmount: 15, status: 'active' }) }, { new: true, runValidators: false } ) }) @@ -160,15 +156,13 @@ describe('helcim subscription endpoint', () => { requireAuth.mockResolvedValue(undefined) requiresPayment.mockReturnValue(true) getHelcimPlanId.mockReturnValue('88888') - getContributionTierByValue.mockReturnValue({ amount: '15' }) - getTierAmount.mockReturnValue(150) const mockMember = { _id: 'member-3', email: 'annual@example.com', name: 'Annual User', circle: 'founder', - contributionTier: '15', + contributionAmount: 15, status: 'active', } Member.findOneAndUpdate.mockResolvedValue(mockMember) @@ -179,36 +173,34 @@ describe('helcim subscription endpoint', () => { const event = createMockEvent({ method: 'POST', path: '/api/helcim/subscription', - body: { customerId: 'cust-1', contributionTier: '15', customerCode: 'code-1', cardToken: 'tok-123', cadence: 'annual' } + body: { customerId: 'cust-1', contributionAmount: 15, customerCode: 'code-1', cardToken: 'tok-123', cadence: 'annual' } }) const result = await subscriptionHandler(event) expect(result.success).toBe(true) expect(createHelcimSubscription).toHaveBeenCalledWith( - expect.objectContaining({ paymentPlanId: 88888, recurringAmount: 150 }), + expect.objectContaining({ paymentPlanId: 88888, recurringAmount: 180 }), 'idem-key-123' ) expect(Member.findOneAndUpdate).toHaveBeenCalledWith( { helcimCustomerId: 'cust-1' }, - { $set: expect.objectContaining({ billingCadence: 'annual', contributionTier: '15', status: 'active' }) }, + { $set: expect.objectContaining({ billingCadence: 'annual', contributionAmount: 15, status: 'active' }) }, { new: true, runValidators: false } ) }) - it('annual $50 tier recurringAmount is 500', async () => { + it('annual $50 tier recurringAmount is 600', async () => { requireAuth.mockResolvedValue(undefined) requiresPayment.mockReturnValue(true) getHelcimPlanId.mockReturnValue('88888') - getContributionTierByValue.mockReturnValue({ amount: '50' }) - getTierAmount.mockReturnValue(500) const mockMember = { _id: 'member-4', email: 'top@example.com', name: 'Top Tier', circle: 'practitioner', - contributionTier: '50', + contributionAmount: 50, status: 'active', } Member.findOneAndUpdate.mockResolvedValue(mockMember) @@ -219,13 +211,13 @@ describe('helcim subscription endpoint', () => { const event = createMockEvent({ method: 'POST', path: '/api/helcim/subscription', - body: { customerId: 'cust-2', contributionTier: '50', customerCode: 'code-2', cardToken: 'tok-456', cadence: 'annual' } + body: { customerId: 'cust-2', contributionAmount: 50, customerCode: 'code-2', cardToken: 'tok-456', cadence: 'annual' } }) await subscriptionHandler(event) expect(createHelcimSubscription).toHaveBeenCalledWith( - expect.objectContaining({ paymentPlanId: 88888, recurringAmount: 500 }), + expect.objectContaining({ paymentPlanId: 88888, recurringAmount: 600 }), 'idem-key-123' ) }) @@ -234,12 +226,11 @@ describe('helcim subscription endpoint', () => { requireAuth.mockResolvedValue(undefined) requiresPayment.mockReturnValue(true) getHelcimPlanId.mockReturnValue(null) - getContributionTierByValue.mockReturnValue({ amount: '15' }) const event = createMockEvent({ method: 'POST', path: '/api/helcim/subscription', - body: { customerId: 'cust-1', contributionTier: '15', customerCode: 'code-1', cardToken: 'tok-123', cadence: 'monthly' } + body: { customerId: 'cust-1', contributionAmount: 15, customerCode: 'code-1', cardToken: 'tok-123', cadence: 'monthly' } }) await expect(subscriptionHandler(event)).rejects.toMatchObject({ @@ -255,12 +246,11 @@ describe('helcim subscription endpoint', () => { requireAuth.mockResolvedValue(undefined) requiresPayment.mockReturnValue(true) getHelcimPlanId.mockReturnValue(null) - getContributionTierByValue.mockReturnValue({ amount: '15' }) const event = createMockEvent({ method: 'POST', path: '/api/helcim/subscription', - body: { customerId: 'cust-1', contributionTier: '15', customerCode: 'code-1', cardToken: 'tok-123', cadence: 'annual' } + body: { customerId: 'cust-1', contributionAmount: 15, customerCode: 'code-1', cardToken: 'tok-123', cadence: 'annual' } }) await expect(subscriptionHandler(event)).rejects.toMatchObject({ @@ -276,8 +266,6 @@ describe('helcim subscription endpoint', () => { requireAuth.mockResolvedValue(undefined) requiresPayment.mockReturnValue(true) getHelcimPlanId.mockReturnValue('99999') - getContributionTierByValue.mockReturnValue({ amount: '15' }) - getTierAmount.mockReturnValue(15) createHelcimSubscription.mockRejectedValue(new Error('Network error')) @@ -286,7 +274,7 @@ describe('helcim subscription endpoint', () => { path: '/api/helcim/subscription', body: { customerId: 'cust-1', - contributionTier: '15', + contributionAmount: 15, customerCode: 'code-1', cardToken: 'tok-123', cadence: 'monthly',