fix(account): show payment history + next-charge for paid-then-$0 members
Three related changes on /member/account:
1. Payment History section now renders when contributionAmount > 0 OR
past payments exist. Previously a paid member who switched to $0 lost
visibility of their own past charges.
2. New "Next charge: $X on DATE" row renders above the transaction list
when nextPaymentDate is available, using --candle dashed border.
3. server/api/helcim/subscription.get.js now reads dateBilling from
Helcim's GET response and handles data as either object or array.
Helcim's real shape is {data: {id, dateBilling, ...}} — the old code
expected {data: [{nextBillingDate}]} and returned empty strings, so
the Membership-card "Next payment" row never rendered for members
whose cached date was missing. subscription.post.js and
update-contribution.post.js have the same wrong field name in their
CREATE flows; left for a follow-up — the GET refresh masks it.
Manual edit-flow and admin-flow tests also recorded in
docs/LAUNCH_READINESS.md.
This commit is contained in:
parent
a80728f0a8
commit
335a4db7cc
3 changed files with 51 additions and 13 deletions
|
|
@ -72,13 +72,18 @@
|
||||||
</div>
|
</div>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|
||||||
<!-- PAYMENT HISTORY (only when there's actually a paid plan) -->
|
<!-- PAYMENT HISTORY (shown when a paid plan is active OR past payments exist) -->
|
||||||
<PageSection
|
<PageSection
|
||||||
v-if="memberData.helcimCustomerId && (memberData.contributionAmount || 0) > 0"
|
v-if="memberData.helcimCustomerId && ((memberData.contributionAmount || 0) > 0 || paymentHistory.length > 0)"
|
||||||
divider="top"
|
divider="top"
|
||||||
>
|
>
|
||||||
<div class="section-label">Payment history</div>
|
<div class="section-label">Payment history</div>
|
||||||
|
|
||||||
|
<div v-if="nextPaymentDate" class="next-charge">
|
||||||
|
<span class="next-charge-label">Next charge</span>
|
||||||
|
<span class="next-charge-value">${{ nextChargeAmount }} on {{ formatNextPaymentDate(nextPaymentDate) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="paymentHistoryLoading" class="history-card">
|
<div v-if="paymentHistoryLoading" class="history-card">
|
||||||
<div class="history-row history-state">Loading…</div>
|
<div class="history-row history-state">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -388,6 +393,12 @@ const currentContributionLabel = computed(() => {
|
||||||
return cadence.value === 'annual' ? `$${displayAmount} / year` : `$${displayAmount} / month`;
|
return cadence.value === 'annual' ? `$${displayAmount} / year` : `$${displayAmount} / month`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nextChargeAmount = computed(() => {
|
||||||
|
const amount = Number(memberData.value?.contributionAmount || 0);
|
||||||
|
if (!amount) return null;
|
||||||
|
return cadence.value === 'annual' ? amount * 12 : amount;
|
||||||
|
});
|
||||||
|
|
||||||
const circleOptions = [
|
const circleOptions = [
|
||||||
{
|
{
|
||||||
value: "community",
|
value: "community",
|
||||||
|
|
@ -947,6 +958,27 @@ const confirmCancelMembership = async () => {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- NEXT CHARGE ---- */
|
||||||
|
.next-charge {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px 1fr;
|
||||||
|
gap: 0 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px dashed var(--candle);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.next-charge-label {
|
||||||
|
color: var(--text-faint);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.next-charge-value {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- PAYMENT HISTORY ---- */
|
/* ---- PAYMENT HISTORY ---- */
|
||||||
.history-card {
|
.history-card {
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ Shipped across `accept-invite.vue`, `join.vue`, `member/account.vue`, `welcome.v
|
||||||
- **"Per Year" / "Per Month"** toggle copy (was "Annual" / "Monthly"). On `/accept-invite`, Per Year is now the default; `/join` stays on Per Month by default.
|
- **"Per Year" / "Per Month"** toggle copy (was "Annual" / "Monthly"). On `/accept-invite`, Per Year is now the default; `/join` stays on Per Month by default.
|
||||||
- **Live billing-summary card** below the contribution input on both signup flows — reads e.g. "You'll be charged $180 today ($15/month × 12). Then $180 every year, until you cancel."
|
- **Live billing-summary card** below the contribution input on both signup flows — reads e.g. "You'll be charged $180 today ($15/month × 12). Then $180 every year, until you cancel."
|
||||||
- **Welcome heading on dashboard** for new signups: `/member/dashboard?welcome=1` renders "Welcome to Ghost Guild, {name}" instead of "Welcome back, {name}". `/welcome` redirect now always carries the param; `/accept-invite` navigates to the dashboard with the param directly.
|
- **Welcome heading on dashboard** for new signups: `/member/dashboard?welcome=1` renders "Welcome to Ghost Guild, {name}" instead of "Welcome back, {name}". `/welcome` redirect now always carries the param; `/accept-invite` navigates to the dashboard with the param directly.
|
||||||
- **$0 member polish on `/member/account`**: Payment History section hidden when `contributionAmount === 0` (stops showing "first charge will appear after your next billing cycle" to members with no charges). Solidarity-Fund sentence in the Danger Zone also hidden at $0.
|
- **$0 member polish on `/member/account`**: Payment History section hidden for $0 members with no prior charges (condition now `contributionAmount > 0 || paymentHistory.length > 0` — fixes a regression where paid-then-$0 members lost visibility of their past payments). Solidarity-Fund sentence in the Danger Zone also hidden at $0.
|
||||||
|
- **Next charge row above payment history** on `/member/account`: When a member has an upcoming charge, a "Next charge: $X on DATE" row renders above the transaction list (dashed `--candle` border). Separate from the existing compact "Next payment" row in the Membership Card summary.
|
||||||
|
- **Fixed `subscription.get.js` Helcim field mapping.** Helcim's GET `/subscriptions/:id` returns `data` as a single object (not array) with the field `dateBilling` (not `nextBillingDate`). The lazy refresh endpoint now handles both shapes — previously it returned empty strings, so neither the Membership-card "Next payment" nor the new "Next charge" row rendered for any member whose cached `nextBillingDate` was missing. Note: `subscription.post.js` and `update-contribution.post.js` still read `subscription.nextBillingDate` from Helcim's CREATE response (same wrong field), which is why the cache was empty to begin with. Left unfixed in this pass — the lazy GET refresh now masks it. Worth cleaning up post-launch.
|
||||||
- **State-aware contribution-change hint** on `/member/account`: "You'll be charged $X today to start your subscription." ($0 → paid) / "Your paid subscription will be cancelled." (paid → $0) / "Changes apply on your next billing cycle." (paid → paid, different amount).
|
- **State-aware contribution-change hint** on `/member/account`: "You'll be charged $X today to start your subscription." ($0 → paid) / "Your paid subscription will be cancelled." (paid → $0) / "Changes apply on your next billing cycle." (paid → paid, different amount).
|
||||||
- **Server-side invite accept** now creates the Helcim customer and sets the auth cookie before returning, for both free and paid branches.
|
- **Server-side invite accept** now creates the Helcim customer and sets the auth cookie before returning, for both free and paid branches.
|
||||||
|
|
||||||
|
|
@ -100,14 +102,14 @@ Cannot be verified by Vitest. Both require a real browser + real Helcim test car
|
||||||
4. `$17` Annual — Helcim `recurringAmount: 204`, `billingCadence: 'annual'`, Mongo stores monthly-equivalent `17`.
|
4. `$17` Annual — Helcim `recurringAmount: 204`, `billingCadence: 'annual'`, Mongo stores monthly-equivalent `17`.
|
||||||
5. `$50` Annual (top preset) — Helcim `recurringAmount: 600`.
|
5. `$50` Annual (top preset) — Helcim `recurringAmount: 600`.
|
||||||
|
|
||||||
- [ ] **Edit flows — `/member/account` as an active paid member:**
|
- [x] **Edit flows — `/member/account` as an active paid member:** ✅ Passed 2026-04-20 against Cleo's Annual subscription (Helcim sub 138682).
|
||||||
- Raise amount ($17 → $30). Confirm `updateHelcimSubscription` called with `recurringAmount: 30` (Monthly) or `360` (Annual).
|
- Raise $15 → $30 annual: `updateHelcimSubscription` hit with `recurringAmount: 360`, Mongo `contributionAmount: 30` (Number).
|
||||||
- Lower amount ($30 → $5). Same assertion at the new values.
|
- Lower $30 → $5 annual: `recurringAmount: 60`, Mongo `contributionAmount: 5` (Number).
|
||||||
- ~~Switch cadence (Monthly $17 ↔ Annual $17).~~ **Deferred from launch.** Server (`update-contribution.post.js:184-189`) explicitly rejects cadence changes on existing subscriptions; no UI toggle exists on `/member/account`. Re-scope post-launch if/when we want to support cadence switch (would need Helcim subscription replacement flow, not a plain update).
|
- ~~Switch cadence (Monthly $17 ↔ Annual $17).~~ **Deferred from launch.** Server (`update-contribution.post.js:184-189`) explicitly rejects cadence changes on existing subscriptions; no UI toggle exists on `/member/account`. Re-scope post-launch if/when we want to support cadence switch (would need Helcim subscription replacement flow, not a plain update).
|
||||||
|
|
||||||
- [ ] **Admin flow — `/admin/members/[id]` edit:**
|
- [x] **Admin flow — `/admin/members/[id]` edit:** ✅ Passed 2026-04-20.
|
||||||
- `contributionAmount` input accepts any non-negative whole dollar. Save writes Number to Mongo.
|
- Changed Cleo $5 → $15 via admin PUT. Mongo wrote `contributionAmount: 15` (Number). `contributionTier` field absent across all 34 members (`countDocuments({ contributionTier: { $exists: true } }) === 0`).
|
||||||
- No chip UI here (admin is plain number input by design).
|
- Known non-blocker: admin edit does not sync the change to Helcim's `recurringAmount`. Admin override is direct Mongo-only by design; had to PATCH Helcim manually to re-sync Cleo post-test. Worth noting in docs or surfacing in admin UI post-launch.
|
||||||
|
|
||||||
**Assert across all flows:**
|
**Assert across all flows:**
|
||||||
- Mongo `contributionAmount` is always `Number`, never `String`.
|
- Mongo `contributionAmount` is always `Number`, never `String`.
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,15 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await getHelcimSubscription(member.helcimSubscriptionId)
|
const response = await getHelcimSubscription(member.helcimSubscriptionId)
|
||||||
const subscription = Array.isArray(response)
|
const data = response?.data
|
||||||
? response[0]
|
const subscription = Array.isArray(data)
|
||||||
: (response?.data?.[0] || response)
|
? data[0]
|
||||||
|
: (data && typeof data === 'object' ? data : response)
|
||||||
|
|
||||||
const nextBillingDate = subscription?.nextBillingDate || null
|
// Helcim's GET /subscriptions/:id returns `dateBilling` (YYYY-MM-DD).
|
||||||
|
// POST /subscriptions responses have sometimes been seen with `nextBillingDate`;
|
||||||
|
// accept both so the refresh works regardless of shape.
|
||||||
|
const nextBillingDate = subscription?.dateBilling || subscription?.nextBillingDate || null
|
||||||
|
|
||||||
if (nextBillingDate) {
|
if (nextBillingDate) {
|
||||||
const parsed = new Date(nextBillingDate)
|
const parsed = new Date(nextBillingDate)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue