Commit graph

364 commits

Author SHA1 Message Date
5d6fcdd78d 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).
2026-04-19 18:32:04 +01:00
4c8aff34bf feat(scripts): add migrate-contribution-amount 2026-04-19 18:31:49 +01:00
74ea932cd2 feat(member): rename contributionTier → contributionAmount (Number) 2026-04-19 18:27:35 +01:00
e4dade18b9 feat(validation): rename contributionTier → contributionAmount in Zod schemas 2026-04-19 18:16:47 +01:00
55af652263 feat(contributions): rewrite server config as preset-based helpers 2026-04-19 18:12:44 +01:00
03eee45cbd refactor(contributions): tighten requiresPayment contract; use findLast 2026-04-19 18:10:57 +01:00
62c606b30a feat(contributions): rewrite client config as preset-based helpers 2026-04-19 18:07:43 +01:00
4da0265935 docs(launch): check off manual tests verified 2026-04-19
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.
2026-04-19 17:24:05 +01:00
e7ad076d6a fix(a11y): raise circle description contrast to WCAG AA
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.
2026-04-19 17:23:19 +01:00
19c77a3ab6 feat(account): in-app payment history + change card
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.
2026-04-19 16:36:19 +01:00
eaff5c6020 feat(activity): add billing_card_updated activity type
Required by POST /api/helcim/update-card to persist audit log entries
when a member updates their card.
2026-04-19 16:30:37 +01:00
101d6a231b feat(billing): add update-card API route with rollback + status gate
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.
2026-04-19 16:29:23 +01:00
4f9c11a755 feat(billing): add payment history API route
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).
2026-04-19 16:26:19 +01:00
6888663148 feat(helcim): add transaction list + card update helpers
- 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).
2026-04-19 16:24:16 +01:00
b6f5ae8c5e docs(launch): P1 — in-app billing management, demote Helcim portal 2026-04-19 13:13:45 +01:00
0ca38e5588 fix(auth): expose helcimCustomerId on /api/auth/member
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.
2026-04-19 13:04:46 +01:00
f2e2cedb67 fix(join): redirect immediately on subscription success
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.
2026-04-19 12:58:07 +01:00
968a127f96 fix(join): move flow overlay outside v-if so it survives auth flip
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.
2026-04-19 12:53:50 +01:00
faa5bcbb3c docs(launch): remove /join UX polish from P1 list 2026-04-19 12:24:22 +01:00
f7c6bd88e7 refactor(join): move success state into overlay; remove step 2/3 UI 2026-04-19 12:23:19 +01:00
1bb59f07be refactor(join): auto-open Helcim modal after form submit 2026-04-19 12:21:10 +01:00
5a4c09f988 feat(join): add flow overlay scaffolding for submit→redirect states 2026-04-19 12:19:01 +01:00
a22a576bff fix(join): raise signup form above supporting copy 2026-04-19 12:16:30 +01:00
67cc488c6a docs(launch): consolidate launch readiness; archive completed P0/P1 2026-04-19 12:14:18 +01:00
36829eb1ef docs(launch): check off Helcim cadence manual tests (4, 5, 6; 3 covered by annual swap) 2026-04-18 22:06:48 +01:00
fd9ce5bc2c fix(ui): disambiguate annual tier labels
"$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.
2026-04-18 22:06:38 +01:00
8ceaebb268 fix(helcim): tolerate empty response body on DELETE (204)
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.
2026-04-18 22:06:33 +01:00
549a849bc0 fix(scripts): helcim plan-create payload shape + empty-GET handling
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.
2026-04-18 20:58:17 +01:00
f8e0cf36ba docs(launch): add annual cadence tests + plan-consolidation runbook step 2026-04-18 18:16:23 +01:00
daea8b65be feat(scripts): helcim plan consolidation migration (dry-run default) 2026-04-18 18:12:43 +01:00
fb337a4277 feat(account): display cadence and annual pricing in tier selector 2026-04-18 18:08:10 +01:00
748a84d001 chore(join): drop unused contributionOptions + formatTierAmount label param 2026-04-18 18:04:54 +01:00
673f881b54 refactor(join): use getTierAmount helper for cadence pricing 2026-04-18 18:02:04 +01:00
cd0d3f7167 feat(join): cadence selector with annual pricing (monthly×10)
Radio-pair cadence selector (Monthly / Annual) added to the join form,
reusing the existing .circle-radio styling. contributionItems computed
reactively; all tier labels and the left-column price list update on
toggle. cadence submitted with the subscription payload. payment-setup
hardcoded to monthly (annual upgrades go through /join).
2026-04-18 17:59:10 +01:00
0eeed94772 feat(contribution): free-to-paid uses cadence plan id, persists billingCadence 2026-04-18 17:37:35 +01:00
e8c81cf062 feat(contribution): paid-to-paid tier swap via recurringAmount PATCH 2026-04-18 17:32:22 +01:00
4b5ea9bbd8 fix(helcim): restore subscriptionStartDate on paid-tier activation 2026-04-18 17:28:05 +01:00
8d43804c7f feat(helcim): create subscription by cadence with recurringAmount
Replace tier-based plan lookup with cadence-keyed lookup, compute
recurringAmount via getTierAmount, persist billingCadence on member.
Delete both manual-fallback blocks; Helcim failure now surfaces as 500.
2026-04-18 17:25:14 +01:00
be0e6e7699 refactor(config): cadence-keyed plan id, add getTierAmount, drop per-tier helcimPlanId 2026-04-18 17:19:05 +01:00
35197c465b feat(schemas): accept cadence field on subscription + contribution updates 2026-04-18 17:16:09 +01:00
47f2d666dd fix(helcim): use Number(id) in wrapped PATCH /subscriptions body 2026-04-18 17:14:24 +01:00
de4bfdcc16 feat(member): add billingCadence field to schema 2026-04-18 17:12:45 +01:00
2ae27d6dda feat(helcim): add cadence-keyed plan id runtime config 2026-04-18 17:10:50 +01:00
c816d4b373 style(payment-setup): drop ColumnsLayout wrapper 2026-04-18 17:06:34 +01:00
4f567e9586 refactor(helcim): wrapped PATCH body, first-activation welcome email guard
Moves updateHelcimSubscription to the live-verified wrapped shape
(PATCH /subscriptions { subscriptions: [{ id, ...payload }] }), adds a prior-
status check so sendWelcomeEmail only fires on pending_payment to active
transitions, short-circuits get-or-create-customer when a valid
helcimCustomerId is already on file, and replaces member.save() Slack-status
writes with findByIdAndUpdate({ runValidators: false }) to avoid save-time
validator pitfalls.
2026-04-18 17:06:30 +01:00
37a58cb0eb feat(member): pending_payment retains access, soften status copy
pending_payment now grants the same RSVP/peer-support capabilities as active,
and status banner/label copy is rewritten to be non-threatening ("Setting up
payment", "Paused", "Closed"). Aligns member-facing copy across the account
page with the capability model.
2026-04-18 17:06:22 +01:00
15329e3e84 refactor(events): gate member benefits on hasMemberAccess
Extracts hasMemberAccess(member) in tickets.js and uses it across event
registration, ticket purchase, and series purchase flows so guest, suspended,
and cancelled records no longer count as members while pending_payment still
does.
2026-04-18 17:06:17 +01:00
c5e901ed24 feat(signup): community guidelines agreement and policies routes
Introduces /community-guidelines and /policies/{privacy,terms,[slug]} pages,
swaps the signup/invite checkbox from agreedToTerms to agreedToGuidelines,
adds Member.agreement.acceptedAt, and stamps the field when a Helcim
customer is created.
2026-04-18 17:06:10 +01:00
e0d11e47f4 chore: remove admin series-management stub actions 2026-04-17 17:27:27 +01:00
2c834da40a chore: remove placeholder payment block from members/create 2026-04-17 17:16:32 +01:00