fix(helcim): read dateBilling on subscription CREATE to populate next-billing cache
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.
This commit is contained in:
parent
3c38333dd1
commit
cf59931814
5 changed files with 65 additions and 9 deletions
|
|
@ -142,7 +142,6 @@ See `docs/TODO.md` for:
|
||||||
|
|
||||||
### Known gotchas worth addressing post-launch
|
### 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.
|
- **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.
|
- **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.
|
- **`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.
|
||||||
|
|
|
||||||
|
|
@ -167,9 +167,8 @@ export default defineEventHandler(async (event) => {
|
||||||
throw createError({ statusCode: 500, statusMessage: 'Subscription creation failed' })
|
throw createError({ statusCode: 500, statusMessage: 'Subscription creation failed' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextBillingDate = subscription.nextBillingDate
|
const rawNextBilling = subscription.dateBilling || subscription.nextBillingDate || null
|
||||||
? new Date(subscription.nextBillingDate)
|
const nextBillingDate = rawNextBilling ? new Date(rawNextBilling) : null
|
||||||
: null
|
|
||||||
|
|
||||||
// Atomically capture pre-update status alongside the write so we can
|
// Atomically capture pre-update status alongside the write so we can
|
||||||
// detect the pending_payment → active transition without a separate read
|
// detect the pending_payment → active transition without a separate read
|
||||||
|
|
@ -216,7 +215,7 @@ export default defineEventHandler(async (event) => {
|
||||||
subscription: {
|
subscription: {
|
||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
status: subscription.status,
|
status: subscription.status,
|
||||||
nextBillingDate: subscription.nextBillingDate
|
nextBillingDate: rawNextBilling
|
||||||
},
|
},
|
||||||
member: {
|
member: {
|
||||||
id: member._id,
|
id: member._id,
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,8 @@ export default defineEventHandler(async (event) => {
|
||||||
throw new Error("No subscription returned in response");
|
throw new Error("No subscription returned in response");
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextBillingDate = subscription.nextBillingDate
|
const rawNextBilling = subscription.dateBilling || subscription.nextBillingDate || null;
|
||||||
? new Date(subscription.nextBillingDate)
|
const nextBillingDate = rawNextBilling ? new Date(rawNextBilling) : null;
|
||||||
: null;
|
|
||||||
|
|
||||||
// Update member record
|
// Update member record
|
||||||
await Member.findByIdAndUpdate(
|
await Member.findByIdAndUpdate(
|
||||||
|
|
@ -149,7 +148,7 @@ export default defineEventHandler(async (event) => {
|
||||||
subscription: {
|
subscription: {
|
||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
status: subscription.status,
|
status: subscription.status,
|
||||||
nextBillingDate: subscription.nextBillingDate,
|
nextBillingDate: rawNextBilling,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,41 @@ describe('helcim subscription endpoint', () => {
|
||||||
expect(Member.findById).toHaveBeenCalledWith('member-2')
|
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 () => {
|
it('annual $15 tier creates subscription with correct paymentPlanId and recurringAmount', async () => {
|
||||||
requireAuth.mockResolvedValue(undefined)
|
requireAuth.mockResolvedValue(undefined)
|
||||||
requiresPayment.mockReturnValue(true)
|
requiresPayment.mockReturnValue(true)
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,30 @@ describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
||||||
expect(result.message).toBe('Successfully upgraded to paid tier')
|
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 () => {
|
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')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue