merge: catch up with feature/helcim-plan-consolidation base

# Conflicts:
#	server/api/auth/member.get.js
#	server/api/members/update-contribution.post.js
#	tests/server/api/update-contribution.test.js
This commit is contained in:
Jennie Robinson Faber 2026-04-19 21:33:40 +01:00
commit 7704557f16
10 changed files with 160 additions and 9 deletions

View file

@ -14,6 +14,7 @@ export default defineEventHandler(async (event) => {
contributionAmount: member.contributionAmount,
billingCadence: member.billingCadence,
helcimCustomerId: member.helcimCustomerId,
nextBillingDate: member.nextBillingDate,
membershipLevel: `${member.circle}-${member.contributionAmount}`,
// Profile fields
pronouns: member.pronouns,

View file

@ -0,0 +1,56 @@
// Refresh the authenticated member's cached nextBillingDate from Helcim.
// The account page calls this only when the stored date is stale (missing,
// past, or within ~24h). On success, writes the fresh date back to the member
// record so subsequent loads can render instantly from /api/auth/member.
//
// On Helcim errors, returns { subscription: null, error: 'unavailable' } (HTTP 200)
// so the client can fall back to the cached value (if any) without crashing.
import { requireAuth } from '../../utils/auth.js'
import { getHelcimSubscription } from '../../utils/helcim.js'
import Member from '../../models/member.js'
import { connectDB } from '../../utils/mongoose.js'
export default defineEventHandler(async (event) => {
const member = await requireAuth(event)
if (!member.helcimSubscriptionId) {
return { subscription: null }
}
try {
const response = await getHelcimSubscription(member.helcimSubscriptionId)
const subscription = Array.isArray(response)
? response[0]
: (response?.data?.[0] || response)
const nextBillingDate = subscription?.nextBillingDate || null
if (nextBillingDate) {
const parsed = new Date(nextBillingDate)
if (!Number.isNaN(parsed.getTime())) {
await connectDB()
await Member.findByIdAndUpdate(
member._id,
{ $set: { nextBillingDate: parsed } },
{ runValidators: false }
)
}
}
return {
subscription: subscription
? {
id: String(subscription.id ?? ''),
status: subscription.status || '',
nextBillingDate: nextBillingDate || '',
}
: null,
}
} catch (error) {
console.error('[subscription.get] Helcim lookup failed', {
helcimSubscriptionId: member.helcimSubscriptionId,
error: error?.message || error,
})
return { subscription: null, error: 'unavailable' }
}
})

View file

@ -165,6 +165,10 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 500, statusMessage: 'Subscription creation failed' })
}
const nextBillingDate = subscription.nextBillingDate
? new Date(subscription.nextBillingDate)
: null
// Update member in database
const member = await Member.findOneAndUpdate(
{ helcimCustomerId: body.customerId },
@ -176,6 +180,9 @@ export default defineEventHandler(async (event) => {
billingCadence: cadence,
subscriptionStartDate: new Date(),
status: 'active',
...(nextBillingDate && !Number.isNaN(nextBillingDate.getTime())
? { nextBillingDate }
: {}),
} },
{ new: true, runValidators: false }
)

View file

@ -34,6 +34,7 @@ export default defineEventHandler(async (event) => {
paymentMethod: 'none',
subscriptionEndDate: new Date(),
},
$unset: { nextBillingDate: 1 },
},
{ runValidators: false }
);

View file

@ -98,6 +98,10 @@ export default defineEventHandler(async (event) => {
throw new Error("No subscription returned in response");
}
const nextBillingDate = subscription.nextBillingDate
? new Date(subscription.nextBillingDate)
: null;
// Update member record
await Member.findByIdAndUpdate(
member._id,
@ -107,6 +111,9 @@ export default defineEventHandler(async (event) => {
paymentMethod: "card",
status: "active",
billingCadence: cadence,
...(nextBillingDate && !Number.isNaN(nextBillingDate.getTime())
? { nextBillingDate }
: {}),
} },
{ runValidators: false }
);
@ -148,7 +155,10 @@ export default defineEventHandler(async (event) => {
// Update member to free tier
await Member.findByIdAndUpdate(
member._id,
{ $set: { contributionAmount: newAmount, helcimSubscriptionId: null, paymentMethod: "none", billingCadence: "monthly" } },
{
$set: { contributionAmount: newAmount, helcimSubscriptionId: null, paymentMethod: "none", billingCadence: "monthly" },
$unset: { nextBillingDate: 1 },
},
{ runValidators: false }
);

View file

@ -21,7 +21,9 @@ const ACTIVITY_TYPES = [
'connection_requested',
'connection_confirmed',
'tag_suggested',
'billing_card_updated'
'billing_card_updated',
'member_onboarding_goal_completed',
'member_onboarding_completed'
]
const activityLogSchema = new mongoose.Schema({

View file

@ -134,6 +134,9 @@ export const createHelcimSubscription = (subscription, idempotencyKey) =>
errorMessage: 'Subscription creation failed'
})
export const getHelcimSubscription = (id) =>
helcimFetch(`/subscriptions/${id}`, { errorMessage: 'Subscription lookup failed' })
export const cancelHelcimSubscription = (id) =>
helcimFetch(`/subscriptions/${id}`, { method: 'DELETE', errorMessage: 'Subscription cancellation failed' })
@ -188,7 +191,13 @@ export async function listHelcimCustomerTransactions(customerCode) {
? response
: (response?.transactions || response?.data || [])
const sorted = [...rows].sort((a, b) => {
const filtered = rows.filter((t) => {
const type = String(t?.type || '').toLowerCase()
const amount = typeof t?.amount === 'number' ? t.amount : Number(t?.amount) || 0
return type !== 'verify' && amount > 0
})
const sorted = [...filtered].sort((a, b) => {
const da = Date.parse(a?.dateCreated || '') || 0
const db = Date.parse(b?.dateCreated || '') || 0
return db - da