fix(invite): persist billingCadence at invite-accept time

Annual-choosing invitees who abandoned between accept and payment were left at billingCadence:'monthly' (the model default) while contributionAmount held an annual-unit value, rendering $180/mo in admin views. Persist the chosen cadence at Member.create time.

accept-invite.vue now sends cadence in the accept POST body; inviteAcceptSchema accepts cadence (defaults 'monthly'); accept.post.js sets billingCadence on create, forced to 'monthly' for $0 members since a free member has no billing relationship.
This commit is contained in:
Jennie Robinson Faber 2026-05-24 15:21:34 +01:00
parent 10a28ac5ef
commit c3b1c59779
4 changed files with 62 additions and 0 deletions

View file

@ -209,3 +209,62 @@ describe('POST /api/members/create — auto-flag wiring (3.8)', () => {
)
})
})
// ---------------------------------------------------------------------------
// billingCadence persistence on /api/invite/accept
//
// Regression: an annual-choosing invitee who abandoned before payment was left
// with billingCadence defaulting to 'monthly' while contributionAmount held an
// annual-unit value (e.g. 180), rendering "$180/mo" in admin views. The handler
// must persist the chosen cadence at Member.create time.
// ---------------------------------------------------------------------------
describe('POST /api/invite/accept — billingCadence persistence', () => {
beforeEach(() => {
vi.clearAllMocks()
PreRegistration.findById.mockResolvedValue({
_id: 'prereg-1',
email: 'invitee@example.com',
status: 'pending'
})
PreRegistration.findByIdAndUpdate.mockResolvedValue(undefined)
Member.findOne.mockResolvedValue(null)
Member.create.mockImplementation(async (data) => ({ _id: 'new-member-via-invite', ...data }))
})
it('persists billingCadence "annual" for an annual paid invite', async () => {
globalThis.validateBody.mockResolvedValue({
token: 'tok',
preRegistrationId: 'prereg-1',
name: 'Annual Invitee',
circle: 'community',
contributionAmount: 180,
cadence: 'annual'
})
createHelcimCustomer.mockResolvedValue({ id: 'cust-annual', customerCode: 'code-annual' })
const event = createMockEvent({ method: 'POST', path: '/api/invite/accept' })
await inviteAcceptHandler(event)
expect(Member.create).toHaveBeenCalledWith(
expect.objectContaining({ contributionAmount: 180, billingCadence: 'annual' })
)
})
it('forces billingCadence "monthly" for a $0 invite even when cadence is annual', async () => {
globalThis.validateBody.mockResolvedValue({
token: 'tok',
preRegistrationId: 'prereg-1',
name: 'Free Invitee',
circle: 'community',
contributionAmount: 0,
cadence: 'annual'
})
const event = createMockEvent({ method: 'POST', path: '/api/invite/accept' })
await inviteAcceptHandler(event)
expect(Member.create).toHaveBeenCalledWith(
expect.objectContaining({ contributionAmount: 0, billingCadence: 'monthly' })
)
})
})