diff --git a/app/pages/accept-invite.vue b/app/pages/accept-invite.vue index 9946b0d..080ee0d 100644 --- a/app/pages/accept-invite.vue +++ b/app/pages/accept-invite.vue @@ -288,7 +288,6 @@ const handleAccept = async () => { circle: form.circle, motivation: form.motivation || undefined, contributionAmount: form.contributionAmount, - cadence: cadence.value, agreedToGuidelines: form.agreedToGuidelines, token: token.value, }, diff --git a/app/pages/join.vue b/app/pages/join.vue index df1934b..e2032f0 100644 --- a/app/pages/join.vue +++ b/app/pages/join.vue @@ -432,7 +432,6 @@ const handleSubmit = async () => { email: form.email, circle: form.circle, contributionAmount: form.contributionAmount, - cadence: cadence.value, agreedToGuidelines: form.agreedToGuidelines, billingAddress: form.billingAddress, }, diff --git a/server/api/helcim/customer.post.js b/server/api/helcim/customer.post.js index 527d67c..28ceda3 100644 --- a/server/api/helcim/customer.post.js +++ b/server/api/helcim/customer.post.js @@ -62,7 +62,6 @@ export default defineEventHandler(async (event) => { name: body.name, circle: body.circle, contributionAmount: body.contributionAmount, - billingCadence: body.contributionAmount === 0 ? 'monthly' : body.cadence, helcimCustomerId: customerData.id, helcimCustomerCode: customerData.customerCode, status: 'pending_payment', @@ -77,7 +76,6 @@ export default defineEventHandler(async (event) => { name: body.name, circle: body.circle, contributionAmount: body.contributionAmount, - billingCadence: body.contributionAmount === 0 ? 'monthly' : body.cadence, helcimCustomerId: customerData.id, helcimCustomerCode: customerData.customerCode, status: 'pending_payment', diff --git a/server/api/invite/accept.post.js b/server/api/invite/accept.post.js index 9a2b7a4..2d6518e 100644 --- a/server/api/invite/accept.post.js +++ b/server/api/invite/accept.post.js @@ -59,7 +59,6 @@ export default defineEventHandler(async (event) => { location: body.location || undefined, circle: body.circle, contributionAmount: body.contributionAmount, - billingCadence: body.contributionAmount === 0 ? 'monthly' : body.cadence, bio: body.motivation || undefined, status: body.contributionAmount === 0 ? 'active' : 'pending_payment', helcimCustomerId: helcimCustomer?.id, diff --git a/server/utils/schemas.js b/server/utils/schemas.js index a60a76a..8504534 100644 --- a/server/utils/schemas.js +++ b/server/utils/schemas.js @@ -63,7 +63,6 @@ export const helcimCustomerSchema = z.object({ email: z.string().trim().toLowerCase().email(), circle: z.enum(['community', 'founder', 'practitioner']).optional().default('community'), contributionAmount: z.number().int().min(0).optional(), - cadence: z.enum(['monthly', 'annual']).default('monthly'), agreedToGuidelines: z.literal(true) }) @@ -364,7 +363,6 @@ export const inviteAcceptSchema = z.object({ circle: z.enum(['community', 'founder', 'practitioner']), motivation: z.string().max(5000).optional(), contributionAmount: z.number().int().min(0), - cadence: z.enum(['monthly', 'annual']).default('monthly'), agreedToGuidelines: z.literal(true), token: z.string().min(1) }) diff --git a/tests/server/api/activation-auto-flag.test.js b/tests/server/api/activation-auto-flag.test.js index 1e39663..2895506 100644 --- a/tests/server/api/activation-auto-flag.test.js +++ b/tests/server/api/activation-auto-flag.test.js @@ -209,62 +209,3 @@ 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' }) - ) - }) -}) diff --git a/tests/server/api/helcim-customer.test.js b/tests/server/api/helcim-customer.test.js index e6a0102..2aa6ae3 100644 --- a/tests/server/api/helcim-customer.test.js +++ b/tests/server/api/helcim-customer.test.js @@ -385,67 +385,4 @@ describe('POST /api/helcim/customer', () => { expect(setAuthCookie).not.toHaveBeenCalled() }) }) - - // Regression: a joiner who picks annual but abandons before - // /api/helcim/subscription runs must already have billingCadence persisted, - // otherwise contributionAmount (cadence-unit, e.g. 180) renders as "$180/mo". - describe('billingCadence persistence', () => { - it('persists billingCadence "annual" when a new member signs up annual', async () => { - Member.findOne.mockResolvedValue(null) - const event = build({ - body: { - name: 'Annual Joiner', - email: 'annual@example.com', - circle: 'community', - contributionAmount: 180, - cadence: 'annual', - agreedToGuidelines: true - } - }) - await customerHandler(event) - expect(Member.create).toHaveBeenCalledWith( - expect.objectContaining({ contributionAmount: 180, billingCadence: 'annual' }) - ) - }) - - it('persists billingCadence "annual" when upgrading a guest who signs up annual', async () => { - Member.findOne.mockResolvedValue({ - _id: 'guest-annual', - email: 'guest@example.com', - status: 'guest' - }) - const event = build({ - body: { - name: 'Guest Annual', - email: 'guest@example.com', - circle: 'founder', - contributionAmount: 180, - cadence: 'annual', - agreedToGuidelines: true - } - }) - await customerHandler(event) - expect(Member.create).not.toHaveBeenCalled() - const [, updatePayload] = Member.findByIdAndUpdate.mock.calls[0] - expect(updatePayload.$set).toMatchObject({ contributionAmount: 180, billingCadence: 'annual' }) - }) - - it('forces billingCadence "monthly" for a $0 signup even when cadence is annual', async () => { - Member.findOne.mockResolvedValue(null) - const event = build({ - body: { - name: 'Free Joiner', - email: 'free@example.com', - circle: 'community', - contributionAmount: 0, - cadence: 'annual', - agreedToGuidelines: true - } - }) - await customerHandler(event) - expect(Member.create).toHaveBeenCalledWith( - expect.objectContaining({ contributionAmount: 0, billingCadence: 'monthly' }) - ) - }) - }) })