diff --git a/app/pages/member/account.vue b/app/pages/member/account.vue index 5bccb66..4f9553a 100644 --- a/app/pages/member/account.vue +++ b/app/pages/member/account.vue @@ -72,13 +72,18 @@ - +
Payment history
+
+ Next charge + ${{ nextChargeAmount }} on {{ formatNextPaymentDate(nextPaymentDate) }} +
+
Loading…
@@ -388,6 +393,12 @@ const currentContributionLabel = computed(() => { 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 = [ { value: "community", @@ -947,6 +958,27 @@ const confirmCancelMembership = async () => { 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 ---- */ .history-card { border: 1px dashed var(--border); diff --git a/docs/LAUNCH_READINESS.md b/docs/LAUNCH_READINESS.md index c83d8b9..775b93c 100644 --- a/docs/LAUNCH_READINESS.md +++ b/docs/LAUNCH_READINESS.md @@ -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. - **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. -- **$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). - **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`. 5. `$50` Annual (top preset) — Helcim `recurringAmount: 600`. - - [ ] **Edit flows — `/member/account` as an active paid member:** - - Raise amount ($17 → $30). Confirm `updateHelcimSubscription` called with `recurringAmount: 30` (Monthly) or `360` (Annual). - - Lower amount ($30 → $5). Same assertion at the new values. + - [x] **Edit flows — `/member/account` as an active paid member:** ✅ Passed 2026-04-20 against Cleo's Annual subscription (Helcim sub 138682). + - Raise $15 → $30 annual: `updateHelcimSubscription` hit with `recurringAmount: 360`, Mongo `contributionAmount: 30` (Number). + - 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). - - [ ] **Admin flow — `/admin/members/[id]` edit:** - - `contributionAmount` input accepts any non-negative whole dollar. Save writes Number to Mongo. - - No chip UI here (admin is plain number input by design). + - [x] **Admin flow — `/admin/members/[id]` edit:** ✅ Passed 2026-04-20. + - Changed Cleo $5 → $15 via admin PUT. Mongo wrote `contributionAmount: 15` (Number). `contributionTier` field absent across all 34 members (`countDocuments({ contributionTier: { $exists: true } }) === 0`). + - 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:** - Mongo `contributionAmount` is always `Number`, never `String`. diff --git a/server/api/helcim/subscription.get.js b/server/api/helcim/subscription.get.js index 99d2bc5..f4ad2b1 100644 --- a/server/api/helcim/subscription.get.js +++ b/server/api/helcim/subscription.get.js @@ -19,11 +19,15 @@ export default defineEventHandler(async (event) => { try { const response = await getHelcimSubscription(member.helcimSubscriptionId) - const subscription = Array.isArray(response) - ? response[0] - : (response?.data?.[0] || response) + const data = response?.data + const subscription = Array.isArray(data) + ? 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) { const parsed = new Date(nextBillingDate)