feat(members): use contributionAmount in update-contribution route, inline ×12
This commit is contained in:
parent
613d077eaa
commit
7a2acd4628
2 changed files with 47 additions and 74 deletions
|
|
@ -2,8 +2,6 @@
|
||||||
import {
|
import {
|
||||||
getHelcimPlanId,
|
getHelcimPlanId,
|
||||||
requiresPayment,
|
requiresPayment,
|
||||||
getContributionTierByValue,
|
|
||||||
getTierAmount,
|
|
||||||
} from "../../config/contributions.js";
|
} from "../../config/contributions.js";
|
||||||
import { connectDB } from "../../utils/mongoose.js";
|
import { connectDB } from "../../utils/mongoose.js";
|
||||||
import Member from "../../models/member.js";
|
import Member from "../../models/member.js";
|
||||||
|
|
@ -22,24 +20,24 @@ export default defineEventHandler(async (event) => {
|
||||||
await connectDB();
|
await connectDB();
|
||||||
const body = await validateBody(event, updateContributionSchema);
|
const body = await validateBody(event, updateContributionSchema);
|
||||||
|
|
||||||
const oldTier = member.contributionTier;
|
const oldAmount = member.contributionAmount;
|
||||||
const newTier = body.contributionTier;
|
const newAmount = body.contributionAmount;
|
||||||
|
|
||||||
// If same tier, nothing to do
|
// If same amount, nothing to do
|
||||||
if (oldTier === newTier) {
|
if (oldAmount === newAmount) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Already on this tier",
|
message: "Already contributing this amount",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log contribution change (fire-and-forget, at the top so it logs regardless of which case path executes)
|
// Log contribution change (fire-and-forget, at the top so it logs regardless of which case path executes)
|
||||||
const logContributionChange = () => {
|
const logContributionChange = () => {
|
||||||
logActivity(member._id, 'contribution_changed', { from: oldTier, to: newTier })
|
logActivity(member._id, 'contribution_changed', { from: oldAmount, to: newAmount })
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldRequiresPayment = requiresPayment(oldTier);
|
const oldRequiresPayment = requiresPayment(oldAmount);
|
||||||
const newRequiresPayment = requiresPayment(newTier);
|
const newRequiresPayment = requiresPayment(newAmount);
|
||||||
|
|
||||||
// Case 1: Moving from free to paid tier
|
// Case 1: Moving from free to paid tier
|
||||||
if (!oldRequiresPayment && newRequiresPayment) {
|
if (!oldRequiresPayment && newRequiresPayment) {
|
||||||
|
|
@ -65,8 +63,6 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tierInfo = getContributionTierByValue(newTier);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const customerData = await getHelcimCustomer(member.helcimCustomerId);
|
const customerData = await getHelcimCustomer(member.helcimCustomerId);
|
||||||
const customerCode = customerData.customerCode;
|
const customerCode = customerData.customerCode;
|
||||||
|
|
@ -90,7 +86,7 @@ export default defineEventHandler(async (event) => {
|
||||||
dateActivated: new Date().toISOString().split("T")[0],
|
dateActivated: new Date().toISOString().split("T")[0],
|
||||||
paymentPlanId: parseInt(paymentPlanId),
|
paymentPlanId: parseInt(paymentPlanId),
|
||||||
customerCode,
|
customerCode,
|
||||||
recurringAmount: getTierAmount(tierInfo, cadence),
|
recurringAmount: cadence === 'annual' ? newAmount * 12 : newAmount,
|
||||||
paymentMethod: "card",
|
paymentMethod: "card",
|
||||||
},
|
},
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
|
|
@ -106,7 +102,7 @@ export default defineEventHandler(async (event) => {
|
||||||
await Member.findByIdAndUpdate(
|
await Member.findByIdAndUpdate(
|
||||||
member._id,
|
member._id,
|
||||||
{ $set: {
|
{ $set: {
|
||||||
contributionTier: newTier,
|
contributionAmount: newAmount,
|
||||||
helcimSubscriptionId: subscription.id,
|
helcimSubscriptionId: subscription.id,
|
||||||
paymentMethod: "card",
|
paymentMethod: "card",
|
||||||
status: "active",
|
status: "active",
|
||||||
|
|
@ -152,7 +148,7 @@ export default defineEventHandler(async (event) => {
|
||||||
// Update member to free tier
|
// Update member to free tier
|
||||||
await Member.findByIdAndUpdate(
|
await Member.findByIdAndUpdate(
|
||||||
member._id,
|
member._id,
|
||||||
{ $set: { contributionTier: newTier, helcimSubscriptionId: null, paymentMethod: "none", billingCadence: "monthly" } },
|
{ $set: { contributionAmount: newAmount, helcimSubscriptionId: null, paymentMethod: "none", billingCadence: "monthly" } },
|
||||||
{ runValidators: false }
|
{ runValidators: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -182,20 +178,15 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTierInfo = getContributionTierByValue(newTier);
|
|
||||||
if (!newTierInfo) {
|
|
||||||
throw createError({ statusCode: 400, statusMessage: 'Invalid tier' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const subscriptionData = await updateHelcimSubscription(
|
const subscriptionData = await updateHelcimSubscription(
|
||||||
member.helcimSubscriptionId,
|
member.helcimSubscriptionId,
|
||||||
{ recurringAmount: getTierAmount(newTierInfo, memberCadence) }
|
{ recurringAmount: memberCadence === 'annual' ? newAmount * 12 : newAmount }
|
||||||
);
|
);
|
||||||
|
|
||||||
await Member.findByIdAndUpdate(
|
await Member.findByIdAndUpdate(
|
||||||
member._id,
|
member._id,
|
||||||
{ $set: { contributionTier: newTier } },
|
{ $set: { contributionAmount: newAmount } },
|
||||||
{ runValidators: false }
|
{ runValidators: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -215,7 +206,7 @@ export default defineEventHandler(async (event) => {
|
||||||
// Case 4: Moving between free tiers (shouldn't happen but handle it)
|
// Case 4: Moving between free tiers (shouldn't happen but handle it)
|
||||||
await Member.findByIdAndUpdate(
|
await Member.findByIdAndUpdate(
|
||||||
member._id,
|
member._id,
|
||||||
{ $set: { contributionTier: newTier } },
|
{ $set: { contributionAmount: newAmount } },
|
||||||
{ runValidators: false }
|
{ runValidators: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import Member from '../../../server/models/member.js'
|
||||||
import {
|
import {
|
||||||
requiresPayment,
|
requiresPayment,
|
||||||
getHelcimPlanId,
|
getHelcimPlanId,
|
||||||
getContributionTierByValue,
|
|
||||||
getTierAmount,
|
|
||||||
} from '../../../server/config/contributions.js'
|
} from '../../../server/config/contributions.js'
|
||||||
import {
|
import {
|
||||||
updateHelcimSubscription,
|
updateHelcimSubscription,
|
||||||
|
|
@ -25,8 +23,6 @@ vi.mock('../../../server/utils/auth.js', () => ({ requireAuth: vi.fn() }))
|
||||||
vi.mock('../../../server/config/contributions.js', () => ({
|
vi.mock('../../../server/config/contributions.js', () => ({
|
||||||
requiresPayment: vi.fn(),
|
requiresPayment: vi.fn(),
|
||||||
getHelcimPlanId: vi.fn(),
|
getHelcimPlanId: vi.fn(),
|
||||||
getContributionTierByValue: vi.fn(),
|
|
||||||
getTierAmount: vi.fn(),
|
|
||||||
}))
|
}))
|
||||||
vi.mock('../../../server/utils/helcim.js', () => ({
|
vi.mock('../../../server/utils/helcim.js', () => ({
|
||||||
getHelcimCustomer: vi.fn(),
|
getHelcimCustomer: vi.fn(),
|
||||||
|
|
@ -55,20 +51,18 @@ describe('update-contribution endpoint — Case 3 (paid→paid)', () => {
|
||||||
it('monthly $5 → $15: calls updateHelcimSubscription with recurringAmount and updates member', async () => {
|
it('monthly $5 → $15: calls updateHelcimSubscription with recurringAmount and updates member', async () => {
|
||||||
const mockMember = {
|
const mockMember = {
|
||||||
_id: 'member-1',
|
_id: 'member-1',
|
||||||
contributionTier: '5',
|
contributionAmount: 5,
|
||||||
helcimSubscriptionId: 'sub-1',
|
helcimSubscriptionId: 'sub-1',
|
||||||
billingCadence: 'monthly',
|
billingCadence: 'monthly',
|
||||||
}
|
}
|
||||||
setMember(mockMember)
|
setMember(mockMember)
|
||||||
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
|
|
||||||
getTierAmount.mockReturnValue(15)
|
|
||||||
updateHelcimSubscription.mockResolvedValue({ id: 'sub-1', status: 'active' })
|
updateHelcimSubscription.mockResolvedValue({ id: 'sub-1', status: 'active' })
|
||||||
Member.findByIdAndUpdate.mockResolvedValue({})
|
Member.findByIdAndUpdate.mockResolvedValue({})
|
||||||
|
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/members/update-contribution',
|
path: '/api/members/update-contribution',
|
||||||
body: { contributionTier: '15' },
|
body: { contributionAmount: 15 },
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await handler(event)
|
const result = await handler(event)
|
||||||
|
|
@ -76,81 +70,76 @@ describe('update-contribution endpoint — Case 3 (paid→paid)', () => {
|
||||||
expect(updateHelcimSubscription).toHaveBeenCalledWith('sub-1', { recurringAmount: 15 })
|
expect(updateHelcimSubscription).toHaveBeenCalledWith('sub-1', { recurringAmount: 15 })
|
||||||
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
|
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
|
||||||
'member-1',
|
'member-1',
|
||||||
{ $set: { contributionTier: '15' } },
|
{ $set: { contributionAmount: 15 } },
|
||||||
{ runValidators: false }
|
{ runValidators: false }
|
||||||
)
|
)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
expect(result.message).toBe('Successfully updated contribution level')
|
expect(result.message).toBe('Successfully updated contribution level')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('annual $5 → $15: calls updateHelcimSubscription with recurringAmount 150', async () => {
|
it('annual $5 → $15: calls updateHelcimSubscription with recurringAmount 180', async () => {
|
||||||
const mockMember = {
|
const mockMember = {
|
||||||
_id: 'member-2',
|
_id: 'member-2',
|
||||||
contributionTier: '5',
|
contributionAmount: 5,
|
||||||
helcimSubscriptionId: 'sub-2',
|
helcimSubscriptionId: 'sub-2',
|
||||||
billingCadence: 'annual',
|
billingCadence: 'annual',
|
||||||
}
|
}
|
||||||
setMember(mockMember)
|
setMember(mockMember)
|
||||||
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
|
|
||||||
getTierAmount.mockReturnValue(150)
|
|
||||||
updateHelcimSubscription.mockResolvedValue({ id: 'sub-2', status: 'active' })
|
updateHelcimSubscription.mockResolvedValue({ id: 'sub-2', status: 'active' })
|
||||||
Member.findByIdAndUpdate.mockResolvedValue({})
|
Member.findByIdAndUpdate.mockResolvedValue({})
|
||||||
|
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/members/update-contribution',
|
path: '/api/members/update-contribution',
|
||||||
body: { contributionTier: '15', cadence: 'annual' },
|
body: { contributionAmount: 15, cadence: 'annual' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await handler(event)
|
const result = await handler(event)
|
||||||
|
|
||||||
expect(updateHelcimSubscription).toHaveBeenCalledWith('sub-2', { recurringAmount: 150 })
|
expect(updateHelcimSubscription).toHaveBeenCalledWith('sub-2', { recurringAmount: 180 })
|
||||||
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
|
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
|
||||||
'member-2',
|
'member-2',
|
||||||
{ $set: { contributionTier: '15' } },
|
{ $set: { contributionAmount: 15 } },
|
||||||
{ runValidators: false }
|
{ runValidators: false }
|
||||||
)
|
)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('annual $15 → $50: calls updateHelcimSubscription with recurringAmount 500', async () => {
|
it('annual $15 → $50: calls updateHelcimSubscription with recurringAmount 600', async () => {
|
||||||
const mockMember = {
|
const mockMember = {
|
||||||
_id: 'member-3',
|
_id: 'member-3',
|
||||||
contributionTier: '15',
|
contributionAmount: 15,
|
||||||
helcimSubscriptionId: 'sub-3',
|
helcimSubscriptionId: 'sub-3',
|
||||||
billingCadence: 'annual',
|
billingCadence: 'annual',
|
||||||
}
|
}
|
||||||
setMember(mockMember)
|
setMember(mockMember)
|
||||||
getContributionTierByValue.mockReturnValue({ value: '50', amount: '50' })
|
|
||||||
getTierAmount.mockReturnValue(500)
|
|
||||||
updateHelcimSubscription.mockResolvedValue({ id: 'sub-3', status: 'active' })
|
updateHelcimSubscription.mockResolvedValue({ id: 'sub-3', status: 'active' })
|
||||||
Member.findByIdAndUpdate.mockResolvedValue({})
|
Member.findByIdAndUpdate.mockResolvedValue({})
|
||||||
|
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/members/update-contribution',
|
path: '/api/members/update-contribution',
|
||||||
body: { contributionTier: '50', cadence: 'annual' },
|
body: { contributionAmount: 50, cadence: 'annual' },
|
||||||
})
|
})
|
||||||
|
|
||||||
await handler(event)
|
await handler(event)
|
||||||
|
|
||||||
expect(updateHelcimSubscription).toHaveBeenCalledWith('sub-3', { recurringAmount: 500 })
|
expect(updateHelcimSubscription).toHaveBeenCalledWith('sub-3', { recurringAmount: 600 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cadence mismatch: monthly member + body cadence annual → 400, no Helcim call, no DB write', async () => {
|
it('cadence mismatch: monthly member + body cadence annual → 400, no Helcim call, no DB write', async () => {
|
||||||
const mockMember = {
|
const mockMember = {
|
||||||
_id: 'member-4',
|
_id: 'member-4',
|
||||||
contributionTier: '5',
|
contributionAmount: 5,
|
||||||
helcimSubscriptionId: 'sub-4',
|
helcimSubscriptionId: 'sub-4',
|
||||||
billingCadence: 'monthly',
|
billingCadence: 'monthly',
|
||||||
}
|
}
|
||||||
setMember(mockMember)
|
setMember(mockMember)
|
||||||
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
|
|
||||||
|
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/members/update-contribution',
|
path: '/api/members/update-contribution',
|
||||||
body: { contributionTier: '15', cadence: 'annual' },
|
body: { contributionAmount: 15, cadence: 'annual' },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(handler(event)).rejects.toMatchObject({
|
await expect(handler(event)).rejects.toMatchObject({
|
||||||
|
|
@ -165,19 +154,17 @@ describe('update-contribution endpoint — Case 3 (paid→paid)', () => {
|
||||||
it('Helcim PATCH failure → 500, member NOT updated', async () => {
|
it('Helcim PATCH failure → 500, member NOT updated', async () => {
|
||||||
const mockMember = {
|
const mockMember = {
|
||||||
_id: 'member-5',
|
_id: 'member-5',
|
||||||
contributionTier: '5',
|
contributionAmount: 5,
|
||||||
helcimSubscriptionId: 'sub-5',
|
helcimSubscriptionId: 'sub-5',
|
||||||
billingCadence: 'monthly',
|
billingCadence: 'monthly',
|
||||||
}
|
}
|
||||||
setMember(mockMember)
|
setMember(mockMember)
|
||||||
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
|
|
||||||
getTierAmount.mockReturnValue(15)
|
|
||||||
updateHelcimSubscription.mockRejectedValue(new Error('Helcim 400'))
|
updateHelcimSubscription.mockRejectedValue(new Error('Helcim 400'))
|
||||||
|
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/members/update-contribution',
|
path: '/api/members/update-contribution',
|
||||||
body: { contributionTier: '15' },
|
body: { contributionAmount: 15 },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(handler(event)).rejects.toMatchObject({
|
await expect(handler(event)).rejects.toMatchObject({
|
||||||
|
|
@ -191,7 +178,7 @@ describe('update-contribution endpoint — Case 3 (paid→paid)', () => {
|
||||||
it('no helcimSubscriptionId → 400 with requiresPaymentSetup, no Helcim call', async () => {
|
it('no helcimSubscriptionId → 400 with requiresPaymentSetup, no Helcim call', async () => {
|
||||||
const mockMember = {
|
const mockMember = {
|
||||||
_id: 'member-6',
|
_id: 'member-6',
|
||||||
contributionTier: '5',
|
contributionAmount: 5,
|
||||||
helcimSubscriptionId: null,
|
helcimSubscriptionId: null,
|
||||||
billingCadence: 'monthly',
|
billingCadence: 'monthly',
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +187,7 @@ describe('update-contribution endpoint — Case 3 (paid→paid)', () => {
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/members/update-contribution',
|
path: '/api/members/update-contribution',
|
||||||
body: { contributionTier: '15' },
|
body: { contributionAmount: 15 },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(handler(event)).rejects.toMatchObject({
|
await expect(handler(event)).rejects.toMatchObject({
|
||||||
|
|
@ -216,8 +203,8 @@ describe('update-contribution endpoint — Case 3 (paid→paid)', () => {
|
||||||
describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
// old tier = free, new tier = paid
|
// old amount = 0 (free), new amount > 0 (paid)
|
||||||
requiresPayment.mockImplementation((tier) => tier !== '0')
|
requiresPayment.mockImplementation((amount) => amount !== 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
function setMember(mockMember) {
|
function setMember(mockMember) {
|
||||||
|
|
@ -226,15 +213,13 @@ describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
||||||
|
|
||||||
const freeMember = {
|
const freeMember = {
|
||||||
_id: 'member-c1',
|
_id: 'member-c1',
|
||||||
contributionTier: '0',
|
contributionAmount: 0,
|
||||||
helcimCustomerId: 'cust-1',
|
helcimCustomerId: 'cust-1',
|
||||||
}
|
}
|
||||||
|
|
||||||
it('monthly: calls createHelcimSubscription with monthly plan id and recurringAmount 15, persists billingCadence monthly', async () => {
|
it('monthly: calls createHelcimSubscription with monthly plan id and recurringAmount 15, persists billingCadence monthly', async () => {
|
||||||
setMember(freeMember)
|
setMember(freeMember)
|
||||||
getHelcimPlanId.mockReturnValue('111')
|
getHelcimPlanId.mockReturnValue('111')
|
||||||
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
|
|
||||||
getTierAmount.mockReturnValue(15)
|
|
||||||
getHelcimCustomer.mockResolvedValue({ customerCode: 'code-1' })
|
getHelcimCustomer.mockResolvedValue({ customerCode: 'code-1' })
|
||||||
listHelcimCustomerCards.mockResolvedValue([{ id: 'card-1' }])
|
listHelcimCustomerCards.mockResolvedValue([{ id: 'card-1' }])
|
||||||
createHelcimSubscription.mockResolvedValue({ data: [{ id: 'sub-new', status: 'active', nextBillingDate: '2026-05-18' }] })
|
createHelcimSubscription.mockResolvedValue({ data: [{ id: 'sub-new', status: 'active', nextBillingDate: '2026-05-18' }] })
|
||||||
|
|
@ -243,7 +228,7 @@ describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/members/update-contribution',
|
path: '/api/members/update-contribution',
|
||||||
body: { contributionTier: '15', cadence: 'monthly' },
|
body: { contributionAmount: 15, cadence: 'monthly' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await handler(event)
|
const result = await handler(event)
|
||||||
|
|
@ -254,18 +239,16 @@ describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
||||||
)
|
)
|
||||||
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
|
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
|
||||||
'member-c1',
|
'member-c1',
|
||||||
{ $set: expect.objectContaining({ billingCadence: 'monthly', contributionTier: '15', helcimSubscriptionId: 'sub-new' }) },
|
{ $set: expect.objectContaining({ billingCadence: 'monthly', contributionAmount: 15, helcimSubscriptionId: 'sub-new' }) },
|
||||||
{ runValidators: false }
|
{ runValidators: false }
|
||||||
)
|
)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
expect(result.message).toBe('Successfully upgraded to paid tier')
|
expect(result.message).toBe('Successfully upgraded to paid tier')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('annual: calls createHelcimSubscription with annual plan id and recurringAmount 150, persists billingCadence annual', async () => {
|
it('annual: calls createHelcimSubscription with annual plan id and recurringAmount 180, persists billingCadence annual', async () => {
|
||||||
setMember(freeMember)
|
setMember(freeMember)
|
||||||
getHelcimPlanId.mockReturnValue('222')
|
getHelcimPlanId.mockReturnValue('222')
|
||||||
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
|
|
||||||
getTierAmount.mockReturnValue(150)
|
|
||||||
getHelcimCustomer.mockResolvedValue({ customerCode: 'code-1' })
|
getHelcimCustomer.mockResolvedValue({ customerCode: 'code-1' })
|
||||||
listHelcimCustomerCards.mockResolvedValue([{ id: 'card-1' }])
|
listHelcimCustomerCards.mockResolvedValue([{ id: 'card-1' }])
|
||||||
createHelcimSubscription.mockResolvedValue({ data: [{ id: 'sub-annual', status: 'active', nextBillingDate: '2027-04-18' }] })
|
createHelcimSubscription.mockResolvedValue({ data: [{ id: 'sub-annual', status: 'active', nextBillingDate: '2027-04-18' }] })
|
||||||
|
|
@ -274,18 +257,18 @@ describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/members/update-contribution',
|
path: '/api/members/update-contribution',
|
||||||
body: { contributionTier: '15', cadence: 'annual' },
|
body: { contributionAmount: 15, cadence: 'annual' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await handler(event)
|
const result = await handler(event)
|
||||||
|
|
||||||
expect(createHelcimSubscription).toHaveBeenCalledWith(
|
expect(createHelcimSubscription).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ paymentPlanId: 222, recurringAmount: 150 }),
|
expect.objectContaining({ paymentPlanId: 222, recurringAmount: 180 }),
|
||||||
'idem-key-123'
|
'idem-key-123'
|
||||||
)
|
)
|
||||||
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
|
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
|
||||||
'member-c1',
|
'member-c1',
|
||||||
{ $set: expect.objectContaining({ billingCadence: 'annual', contributionTier: '15', helcimSubscriptionId: 'sub-annual' }) },
|
{ $set: expect.objectContaining({ billingCadence: 'annual', contributionAmount: 15, helcimSubscriptionId: 'sub-annual' }) },
|
||||||
{ runValidators: false }
|
{ runValidators: false }
|
||||||
)
|
)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
|
|
@ -294,12 +277,11 @@ describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
||||||
it('missing plan id env → 500, createHelcimSubscription NOT called, member NOT updated', async () => {
|
it('missing plan id env → 500, createHelcimSubscription NOT called, member NOT updated', async () => {
|
||||||
setMember(freeMember)
|
setMember(freeMember)
|
||||||
getHelcimPlanId.mockReturnValue(null)
|
getHelcimPlanId.mockReturnValue(null)
|
||||||
getContributionTierByValue.mockReturnValue({ value: '15', amount: '15' })
|
|
||||||
|
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/members/update-contribution',
|
path: '/api/members/update-contribution',
|
||||||
body: { contributionTier: '15', cadence: 'monthly' },
|
body: { contributionAmount: 15, cadence: 'monthly' },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(handler(event)).rejects.toMatchObject({
|
await expect(handler(event)).rejects.toMatchObject({
|
||||||
|
|
@ -315,8 +297,8 @@ describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
||||||
describe('update-contribution endpoint — Case 2 (paid→free)', () => {
|
describe('update-contribution endpoint — Case 2 (paid→free)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
// old tier = paid, new tier = free
|
// old amount > 0 (paid), new amount = 0 (free)
|
||||||
requiresPayment.mockImplementation((tier) => tier !== '0')
|
requiresPayment.mockImplementation((amount) => amount !== 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
function setMember(mockMember) {
|
function setMember(mockMember) {
|
||||||
|
|
@ -326,7 +308,7 @@ describe('update-contribution endpoint — Case 2 (paid→free)', () => {
|
||||||
it('cancels subscription, resets billingCadence to monthly, clears helcimSubscriptionId', async () => {
|
it('cancels subscription, resets billingCadence to monthly, clears helcimSubscriptionId', async () => {
|
||||||
const mockMember = {
|
const mockMember = {
|
||||||
_id: 'member-c2',
|
_id: 'member-c2',
|
||||||
contributionTier: '15',
|
contributionAmount: 15,
|
||||||
helcimSubscriptionId: 'sub-1',
|
helcimSubscriptionId: 'sub-1',
|
||||||
billingCadence: 'monthly',
|
billingCadence: 'monthly',
|
||||||
}
|
}
|
||||||
|
|
@ -337,7 +319,7 @@ describe('update-contribution endpoint — Case 2 (paid→free)', () => {
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/members/update-contribution',
|
path: '/api/members/update-contribution',
|
||||||
body: { contributionTier: '0' },
|
body: { contributionAmount: 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await handler(event)
|
const result = await handler(event)
|
||||||
|
|
@ -346,7 +328,7 @@ describe('update-contribution endpoint — Case 2 (paid→free)', () => {
|
||||||
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
|
expect(Member.findByIdAndUpdate).toHaveBeenCalledWith(
|
||||||
'member-c2',
|
'member-c2',
|
||||||
{ $set: expect.objectContaining({
|
{ $set: expect.objectContaining({
|
||||||
contributionTier: '0',
|
contributionAmount: 0,
|
||||||
helcimSubscriptionId: null,
|
helcimSubscriptionId: null,
|
||||||
paymentMethod: 'none',
|
paymentMethod: 'none',
|
||||||
billingCadence: 'monthly',
|
billingCadence: 'monthly',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue