From cf59931814b051592a08683c032a0551af0b5339 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Mon, 27 Apr 2026 19:44:35 +0100 Subject: [PATCH] fix(helcim): read dateBilling on subscription CREATE to populate next-billing cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Helcim returns next-charge as `dateBilling` on POST /subscriptions, but the two CREATE sites were reading `subscription.nextBillingDate`, leaving `member.nextBillingDate` empty after every signup and free→paid upgrade. The lazy refresh in subscription.get.js (which already accepts both shapes) masked it on next account-page load, so renders eventually populated — but the success response we returned to the client also had `nextBillingDate: undefined`. Mirror the GET-side resolution at both CREATE sites: prefer `dateBilling`, fall back to `nextBillingDate`. Existing Number.isNaN guard unchanged; defensively rejects malformed strings from either field. --- docs/LAUNCH_READINESS.md | 1 - server/api/helcim/subscription.post.js | 7 ++-- .../api/members/update-contribution.post.js | 7 ++-- tests/server/api/helcim-subscription.test.js | 35 +++++++++++++++++++ tests/server/api/update-contribution.test.js | 24 +++++++++++++ 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/docs/LAUNCH_READINESS.md b/docs/LAUNCH_READINESS.md index b1e92bb..d75256c 100644 --- a/docs/LAUNCH_READINESS.md +++ b/docs/LAUNCH_READINESS.md @@ -142,7 +142,6 @@ See `docs/TODO.md` for: ### Known gotchas worth addressing post-launch -- **Subscription cache fed wrong field on CREATE.** `subscription.post.js` and `update-contribution.post.js` read `subscription.nextBillingDate` from Helcim's CREATE response, but Helcim returns `dateBilling`. The lazy refresh in `subscription.get.js` masks this (handles both shapes), so next-charge rendering works — but the cache starts empty. Fix at the CREATE sites so the cache is correct from first write. - **Admin edit does not sync Helcim `recurringAmount`.** `/admin/members/[id]` PUT writes `contributionAmount` direct to Mongo by design. Admins must PATCH Helcim manually. Worth surfacing in admin UI or docs. - **Cadence switch rejected on active subscriptions.** `update-contribution.post.js:184-189` refuses cadence changes mid-subscription; no UI toggle exists on `/member/account`. Adding cadence switch would require a Helcim subscription replacement flow, not a plain update. - **`SeriesPassPurchase.vue` doesn't auto-refresh after purchase.** (Observed 2026-04-21 during Phase 4 series-pass functional tests.) Component's local `$fetch` to `/api/series/{id}/tickets/available` fires on mount + `userEmail` watch, but isn't re-invoked after a successful purchase — the "already registered" state only appears on next navigation. Parent page calls `refreshNuxtData()` but the component doesn't participate in it. Fix: call `fetchPassInfo()` after the success toast in `handleSubmit`, or lift the fetch to `useAsyncData` so it can be refreshed from outside. diff --git a/server/api/helcim/subscription.post.js b/server/api/helcim/subscription.post.js index db4ffb7..c4eb977 100644 --- a/server/api/helcim/subscription.post.js +++ b/server/api/helcim/subscription.post.js @@ -167,9 +167,8 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 500, statusMessage: 'Subscription creation failed' }) } - const nextBillingDate = subscription.nextBillingDate - ? new Date(subscription.nextBillingDate) - : null + const rawNextBilling = subscription.dateBilling || subscription.nextBillingDate || null + const nextBillingDate = rawNextBilling ? new Date(rawNextBilling) : null // Atomically capture pre-update status alongside the write so we can // detect the pending_payment → active transition without a separate read @@ -216,7 +215,7 @@ export default defineEventHandler(async (event) => { subscription: { subscriptionId: subscription.id, status: subscription.status, - nextBillingDate: subscription.nextBillingDate + nextBillingDate: rawNextBilling }, member: { id: member._id, diff --git a/server/api/members/update-contribution.post.js b/server/api/members/update-contribution.post.js index 695e7c9..0e4d1f9 100644 --- a/server/api/members/update-contribution.post.js +++ b/server/api/members/update-contribution.post.js @@ -100,9 +100,8 @@ export default defineEventHandler(async (event) => { throw new Error("No subscription returned in response"); } - const nextBillingDate = subscription.nextBillingDate - ? new Date(subscription.nextBillingDate) - : null; + const rawNextBilling = subscription.dateBilling || subscription.nextBillingDate || null; + const nextBillingDate = rawNextBilling ? new Date(rawNextBilling) : null; // Update member record await Member.findByIdAndUpdate( @@ -149,7 +148,7 @@ export default defineEventHandler(async (event) => { subscription: { subscriptionId: subscription.id, status: subscription.status, - nextBillingDate: subscription.nextBillingDate, + nextBillingDate: rawNextBilling, }, }; } catch (error) { diff --git a/tests/server/api/helcim-subscription.test.js b/tests/server/api/helcim-subscription.test.js index 24f8737..8e6bc77 100644 --- a/tests/server/api/helcim-subscription.test.js +++ b/tests/server/api/helcim-subscription.test.js @@ -166,6 +166,41 @@ describe('helcim subscription endpoint', () => { expect(Member.findById).toHaveBeenCalledWith('member-2') }) + it('reads dateBilling from Helcim CREATE response and threads it to $set + response', async () => { + requireAuth.mockResolvedValue(undefined) + requiresPayment.mockReturnValue(true) + getHelcimPlanId.mockReturnValue('99999') + + const mockMember = { + _id: 'member-db', + email: 'datebilling@example.com', + name: 'DateBilling User', + circle: 'founder', + contributionAmount: 15, + status: 'active', + } + Member.findOneAndUpdate.mockResolvedValue({ _id: 'member-db', status: 'pending_payment' }) + Member.findById.mockResolvedValue(mockMember) + createHelcimSubscription.mockResolvedValue({ + data: [{ id: 'sub-x', status: 'active', dateBilling: '2026-06-01' }] + }) + + const event = createMockEvent({ + method: 'POST', + path: '/api/helcim/subscription', + body: { customerId: 'cust-1', contributionAmount: 15, customerCode: 'code-1', cardToken: 'tok-123', cadence: 'monthly' } + }) + + const result = await subscriptionHandler(event) + + expect(Member.findOneAndUpdate).toHaveBeenCalledWith( + { helcimCustomerId: 'cust-1' }, + { $set: expect.objectContaining({ nextBillingDate: new Date('2026-06-01') }) }, + { new: false, runValidators: false, projection: { status: 1 } } + ) + expect(result.subscription.nextBillingDate).toBe('2026-06-01') + }) + it('annual $15 tier creates subscription with correct paymentPlanId and recurringAmount', async () => { requireAuth.mockResolvedValue(undefined) requiresPayment.mockReturnValue(true) diff --git a/tests/server/api/update-contribution.test.js b/tests/server/api/update-contribution.test.js index a62e243..0519c44 100644 --- a/tests/server/api/update-contribution.test.js +++ b/tests/server/api/update-contribution.test.js @@ -261,6 +261,30 @@ describe('update-contribution endpoint — Case 1 (free→paid)', () => { expect(result.message).toBe('Successfully upgraded to paid tier') }) + it('reads dateBilling from Helcim CREATE response and threads it to $set + response', async () => { + setMember(freeMember) + getHelcimPlanId.mockReturnValue('111') + getHelcimCustomer.mockResolvedValue({ customerCode: 'code-1' }) + listHelcimCustomerCards.mockResolvedValue([{ id: 'card-1' }]) + createHelcimSubscription.mockResolvedValue({ data: [{ id: 'sub-new', status: 'active', dateBilling: '2026-05-18' }] }) + Member.findByIdAndUpdate.mockResolvedValue({}) + + const event = createMockEvent({ + method: 'POST', + path: '/api/members/update-contribution', + body: { contributionAmount: 15, cadence: 'monthly' }, + }) + + const result = await handler(event) + + expect(Member.findByIdAndUpdate).toHaveBeenCalledWith( + 'member-c1', + { $set: expect.objectContaining({ nextBillingDate: new Date('2026-05-18') }) }, + { runValidators: false } + ) + expect(result.subscription.nextBillingDate).toBe('2026-05-18') + }) + it('annual: calls createHelcimSubscription with annual plan id and recurringAmount 180, persists billingCadence annual', async () => { setMember(freeMember) getHelcimPlanId.mockReturnValue('222')