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.
Extract shared SignupFlowOverlay component. Static "Monthly Contribution"
label on all three contribution inputs (was misleadingly dynamic).
"Per Year"/"Per Month" toggle copy; Per Year default on accept-invite,
Per Month default on join. Live billing-summary card on both signup
flows. Welcome-heading on dashboard via ?welcome=1 for new signups.
$0-member polish on account page (hide payment-history + Solidarity
Fund prompts). State-aware contribution-change hint. Invite accept now
creates Helcim customer and sets auth cookie server-side for both free
and paid branches. Pre-registrant invite + /join signup flows manually
verified against Cleo Nguyen preReg and $0-$50 variants.
Branch merges and 7/9 manual tests are done — moved to archive. Live
doc now only carries open work: charitable receipts Phase 1, prod
contribution-amount migration + Helcim plan env vars, and two manual
tests (pre-registrant invite, contribution-amount end-to-end). Both
remaining tests now include setup, test steps, assertions, and the
file references needed to complete them without additional context.
Un-defer pre-registrant invite manual test (refactor landed), add
contribution-amount end-to-end manual test, and list the cosmetic
cleanup items (admin column, dead TierPicker, stale comments) in the
post-launch backlog.
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).
Guest signup, mobile responsive, WCAG contrast, and in-app payment
history all verified via tunnel. Payment history's per-row receipt
link requirement accepted as satisfied by the 'Advanced billing in
Helcim' escape hatch (Helcim's card-transactions API doesn't expose
per-row receipt URLs). Also corrects the mobile breakpoint note —
chrome sidebar hides at 768px, in-page columns collapse at 1024px.
The .circle-desc text used --text-faint, which failed WCAG AA on the
selected/hover tile surfaces (4.01:1 light / 4.31:1 dark). Promote to
--text-dim to clear 4.5:1 against all tile states.
Add Payment history section (live-read from Helcim, with loading/empty/error states)
and Change card flow (HelcimPay.js zero-dollar auth -> POST /api/helcim/update-card)
to /member/account. Relabel Helcim portal link to "Advanced billing in Helcim →"
and demote it to a secondary link at the bottom of the billing group.
POST /api/helcim/update-card updates the customer's default card, then
best-effort patches the active subscription payment method. Status-gated
to {active, pending_payment}; verifies the submitted cardToken is
attached to the member's helcimCustomerId via listHelcimCustomerCards.
On subscription PATCH 5xx we revert the customer default to the prior
card token; 4xx (schema rejection — cardToken is not a documented
subscription PATCH field) is tolerated since the customer default is
the authoritative billing driver.
Add GET /api/helcim/payment-history returning the authenticated
member's normalized Helcim card transactions (newest first, capped
at 50). Resolves helcimCustomerId -> customerCode via getHelcimCustomer
before calling listHelcimCustomerTransactions. Returns
{ transactions: [] } when the member has no helcimCustomerId, and
{ transactions: [], error: 'unavailable' } (HTTP 200) on Helcim
failures so the UI can render fallback copy.
Covered by unit tests at tests/server/api/helcim-payment-history.test.js
(auth, missing customer id, happy path, both Helcim failure paths,
missing customerCode).
- listHelcimCustomerTransactions(customerCode): GET /card-transactions/
with customerCode filter, sorts newest-first, caps at 50, normalizes
Helcim status (APPROVED/DECLINED) + type (refund) into
paid/refunded/failed/other.
- updateHelcimCustomerDefaultPaymentMethod(customerId, cardToken):
resolves cardToken -> cardId via /customers/{id}/cards, then PATCHes
/customers/{id}/cards/{cardId}/default.
- updateHelcimSubscriptionPaymentMethod(subscriptionId, cardToken):
wraps updateHelcimSubscription with a cardToken payload.
- helcimUpdateCardSchema: Zod schema { cardToken: string } for the
upcoming /api/helcim/update-card route.
- Unit tests for all three helpers (success + error paths).
The member account page gates the Helcim customer portal link on
`memberData.helcimCustomerId`, but this endpoint (the source for
`useAuth().memberData`) omitted the field, so the link hid for every
member regardless of Helcim enrollment. Add the field to the response.
Removes the 3-second setTimeout that deferred navigateTo('/welcome').
The overlay success state was a holdover from the pre-refactor Step-3
inline block; now that /welcome is the single welcome surface, the
delay just stalls a completed action and fights the continuous-flow
goal of the overlay.
After createSubscription() calls checkMemberStatus(), isAuthenticated
flips to true and the <template v-else> branch unmounts, taking the
Teleport (and its overlay) with it. The authenticated 'You're already a
member' UI then showed for the 3-second pre-redirect delay, producing a
visible flash before navigateTo('/welcome') fired.
Teleport now lives at the root div alongside the v-if/v-else branches,
so the overlay stays mounted through the auth state transition and
covers the page continuously until the redirect.
"$50/yr" was ambiguous — could mean the $5 tier in annual mode or the
$50 tier in monthly mode. On /join the dropdown now shows both prices
("$5/mo → $50/yr") in annual mode. On the account page TierPicker
gains a subtitle slot; annual mode shows "$N/mo tier" beneath the
annual price so members recognize which tier they're on.
helcimFetch called response.json() unconditionally, which threw
"Unexpected end of JSON input" on Helcim's 204 No Content responses
(e.g. DELETE /subscriptions/:id). The error was silently swallowed by
the best-effort cancel path in cancel-subscription, masking cases where
the Helcim-side cancel actually succeeded.
Verified against the live Helcim v2 API during the deploy migration:
- POST /payment-plans requires { paymentPlans: [plan] } wrapper (mirrors
the POST /subscriptions shape), and response is { data: [plan] }.
- taxType 'customer' rejects as ERR_VALIDATION_FAILED; must be 'no_tax'
with taxCalculation 'country_province'.
- termLength:1 rejects when termType:'forever' — drop the field.
- GET /subscriptions returns an empty body (not JSON) when no subs exist;
tolerate that instead of failing with 'Unexpected end of JSON input'.
Plans created in the Helcim account: Monthly=50302, Annual=50303.