feat(account): show next payment date with lazy Helcim refresh
Persist nextBillingDate on subscription create/update; unset on cancel or downgrade to free. Account page displays the cached date and lazily refreshes from Helcim when the cached value is within 24h of now (or missing).
This commit is contained in:
parent
4da0265935
commit
5d6fcdd78d
8 changed files with 146 additions and 3 deletions
|
|
@ -14,6 +14,7 @@ export default defineEventHandler(async (event) => {
|
|||
contributionTier: member.contributionTier,
|
||||
billingCadence: member.billingCadence,
|
||||
helcimCustomerId: member.helcimCustomerId,
|
||||
nextBillingDate: member.nextBillingDate,
|
||||
membershipLevel: `${member.circle}-${member.contributionTier}`,
|
||||
// Profile fields
|
||||
pronouns: member.pronouns,
|
||||
|
|
|
|||
56
server/api/helcim/subscription.get.js
Normal file
56
server/api/helcim/subscription.get.js
Normal 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' }
|
||||
}
|
||||
})
|
||||
|
|
@ -163,6 +163,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 },
|
||||
|
|
@ -174,6 +178,9 @@ export default defineEventHandler(async (event) => {
|
|||
billingCadence: cadence,
|
||||
subscriptionStartDate: new Date(),
|
||||
status: 'active',
|
||||
...(nextBillingDate && !Number.isNaN(nextBillingDate.getTime())
|
||||
? { nextBillingDate }
|
||||
: {}),
|
||||
} },
|
||||
{ new: true, runValidators: false }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export default defineEventHandler(async (event) => {
|
|||
paymentMethod: 'none',
|
||||
subscriptionEndDate: new Date(),
|
||||
},
|
||||
$unset: { nextBillingDate: 1 },
|
||||
},
|
||||
{ runValidators: false }
|
||||
);
|
||||
|
|
|
|||
|
|
@ -102,6 +102,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,
|
||||
|
|
@ -111,6 +115,9 @@ export default defineEventHandler(async (event) => {
|
|||
paymentMethod: "card",
|
||||
status: "active",
|
||||
billingCadence: cadence,
|
||||
...(nextBillingDate && !Number.isNaN(nextBillingDate.getTime())
|
||||
? { nextBillingDate }
|
||||
: {}),
|
||||
} },
|
||||
{ runValidators: false }
|
||||
);
|
||||
|
|
@ -152,7 +159,10 @@ export default defineEventHandler(async (event) => {
|
|||
// Update member to free tier
|
||||
await Member.findByIdAndUpdate(
|
||||
member._id,
|
||||
{ $set: { contributionTier: newTier, helcimSubscriptionId: null, paymentMethod: "none", billingCadence: "monthly" } },
|
||||
{
|
||||
$set: { contributionTier: newTier, helcimSubscriptionId: null, paymentMethod: "none", billingCadence: "monthly" },
|
||||
$unset: { nextBillingDate: 1 },
|
||||
},
|
||||
{ runValidators: false }
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue