Cancellations & refunds — where we are and what’s left
The rules live in docs/reference/israeli-cancellation-rules.md
(source: אמון הציבור, https://www.emun.org/cancellation-guide/ ). This document is only the
engineering picture: what the code does, what’s fixed, what’s left.
Supersedes israeli-cancellation-law-audit.md, whose Part A was assembled from Wikisource
transcriptions and got the scope of the gym fee rule wrong. Its Part B (code audit) was the
basis of the “still open” table this document used to carry; that table is now closed out below.
What Taikan sells, and which regime each falls under
| Product | Regime | Consequence |
|---|---|---|
| Rolling monthly subscription | עסקה מתמשכת — continuous | Cancellable any time; charging stops within 3 days of notice; the shipped effective date is one calendar month from notice (see “The one-month question” below) |
| Punch card (one-off, with expiry) | Not continuous — one-off service purchase | 14 days from purchase or the written document, whichever is later |
| Drop-in (single session) | Not continuous | 14 days, but notice must land ≥2 days before the session |
No committed-term memberships. That matters more than anything else below.
The cancellation fee is structurally zero
The gym cancellation fee is (committed monthly price − alternative-track monthly price) × months elapsed, capped by thirds at 25/20/17%. It exists to let a gym recover the discount a member got
for committing. A rolling monthly plan has no committed price and no alternative track, so the
difference is zero and the caps never bind.
There is no fee engine to build. No alternative_monthly_price column, no thirds arithmetic,
no settlement table. This was the largest single item in the original audit and it is gone on the
merits, not merely deferred.
What this branch ships
A cancellation is a notice, not a request. The gym cannot refuse, gate, or delay it — there is
no approval step anywhere in the shipped code, and no cancellation_requests table. This is P0/P1
of the product plan (“the notice window”); P2–P4 below are not in this branch.
P0 — correctness fixes and silent paths, unflagged:
| Was | Now |
|---|---|
previewCancellation’s alreadyRefunded was always false — the query filtered status='completed', which a transaction no longer carries once its OWN refund has moved it to refund_pending/refunded | Fetches the last charge/recurring transaction regardless of status; alreadyRefunded = status IN (refund_pending, refunded), refundableInCents forced to 0 when so |
refund() always resolved the org’s CURRENT active provider’s adapter and credentials, regardless of which provider actually processed the transaction | Resolves credentials for the transaction’s OWN provider column; refuses with 409 REFUND_PROVIDER_MISMATCH before any adapter call when it differs from the active config and no historical config is on file |
memberCancelAtPeriodEnd’s write had no race guard — two concurrent notices (or a notice racing the period-end sweep) could both land | WHERE … AND cancel_at_period_end=false AND status IN (…); a losing write gets 409 CANCELLATION_ALREADY_SCHEDULED, not a silent double-apply |
memberResumeAtPeriodEnd required status==='active' — a paused/past_due/debt member who gave notice had no way back | Accepts every status a notice can be given against |
sweepDueCancellations never called paymentService.cancelRecurring — an org’s own gateway kept debiting a member past their effective date forever, with nothing anywhere to show for it | Extracted into cancelProviderStandingOrder, shared by the sweep and the staff immediate-cancel path; non-blocking, opens a reminder task and emits payment.provider_cancel_failed on failure |
A manual_refund task’s due date passing changed nothing — no escalation signal while the member’s money sat in limbo | Daily RefundTaskSweepService bumps an overdue task to urgent priority and emits payment.refund_task_overdue |
P1 — retention cases: always-on staff-owned cases for notices, presale withdrawals and opening-day declines. Notices open urgent SLA-timed tasks and enter the daily digest; presale withdrawals open low-priority, no-deadline records with immediate owner email. Opening-day declines open urgent payment-follow-up tasks. Escalation and repeat visits retain their existing eligibility and idempotency. See Retention case.
Also in this branch, unflagged: a scheduled presale subscription can swap to another plan in
its own plan group on the same row (seat, buyer order, token, and opening day all preserved — see
Presale swap),
and a shared cancellation.* observability event family (server) plus member_cancel_*/
staff_cancel_*/staff_case_* events (client), typed and shared by API, web, and native — see
event-tracking/README.md.
Always-on launch behavior
Retention, cancellation-form prompting, provider standing-order cancellation and settlement
run for every eligible organization. A published cancellation form is returned for signing;
when no form is available the ID remains null and notice still succeeds. Signing never
delays cancellation. Current member terms are required from every active member through
the existing /accept-terms gate, not compliance forms. Already-current privacy and fitness
acceptance stays valid. Joining collects applicable organization-scoped external acknowledgement.
Staff create/invite never validates or synthesizes member consent; members accept at first login.
Israeli counsel review is a launch prerequisite.
The presale plan swap is a mode of the same PlanChangeService every other plan-change entry
point goes through, gated the same way they are: role/ownership only. Note: the rest of this
doc set (and the original implementation plan) describes plan change as sitting behind
admin-plan-change/member-plan-change PostHog flags — as of this branch’s base, neither key
exists in libs/shared/src/lib/constants/feature-flags.ts and nothing in apps/api/apps/web
reads them, so plan change (presale swap included) is currently unconditional. That’s a
pre-existing discrepancy between the doc set and the code, not something this branch changed. The
P0 correctness fixes and the overdue-refund-task sweep ship unflagged on purpose (failing toward
correctness, not a feature to toggle — see CLAUDE.md’s feature-flag policy).
What P2–P4 still need
- P2 — settlement (money at the effective date). Always-on proportional prepaid refund routing and final-tail collection follow the stored effective date, verified payment eligibility, audit claims and locking described in Settlement. Manual/off-platform and externally billed amounts remain the organization’s responsibility. Failed or unknown payment outcomes never postpone cancellation or justify an automatic duplicate charge. Counsel review of the disclosed contract is required before launch.
- P3 — “Talk to us first.” A retention CASE (this branch) is not the same thing as a retention FLOW that intercepts the notice before it’s recorded. Whether Taikan should offer a save attempt before the notice is accepted — rather than the current design, where the notice always takes effect immediately and the case is a parallel save attempt against a clock already running — is a product decision, not an engineering one, and isn’t built.
- P4 — compliance. Legal identity on
organizations(country, legal entity name, ח.פ/ע.מ) so the required business-identity and cancellation-channel disclosures can be rendered on an invoice or receipt at all (see rules doc §6); a public, signed-out cancellation intake for a member who cannot sign in (rules doc §5 mandates phone/email regardless of login state); the 14-day cooling-off window for punch cards and drop-ins, which needs acontract_document_delivered_attimestamp to start the clock lawfully; and capturing an ID number on a notice, which Israeli law entitles a business to ask for and Taikan does not currently capture.
The one-month question
This was, and still is, a lawyer question, not an engineering one — Q1 below. The shipped
effective date (computeCancellationEffectiveAt, one calendar month from notice) is a
product/engineering judgment call made to ship P0/P1 rather than a resolved legal position. It is
implemented as one pure function over an explicit stored notice date (cancellation_notice_at),
specifically so that if the answer changes, it changes in one function, not the schema — see
libs/shared/src/lib/cancellation-effective-date.ts.
Counsel questions
Q1 — which effective date governs a rolling monthly membership? The guidance states the one-month rule for gym memberships without limiting it to fixed terms, but states it beside a fee formula that only makes sense for a committed term. The continuous-transaction rule says charging stops within 3 days of notice. For a rolling monthly plan these give different answers, and the difference is up to a month of membership. This is the one thing the shipped one-month effective date is a placeholder for pending an answer.
Q2 — 3 days or 3 business days? The guidance says 3 days; the statute reportedly says three business days. Implemented as 3 calendar days: it is the shorter and safer of the two, and removes the need for an Israeli business-day calendar entirely.
Q3 — do we accept notices from people who cannot sign in? Email and phone are mandatory channels regardless of login state, which implies a public intake and an unmatched-notice queue. That is a new abuse surface and a product decision, and it’s the P4 compliance item above.
Q4 — does the distance-sale 14-day cooling-off (§1) stack on top of the gym-membership one-month
rule (§4), or does the more specific gym rule fully replace it for this product? If both apply, a
member who signs up online gets a separate no-questions 14-day window before the one-month notice
regime even starts. Nothing shipped implements a distinct cooling-off window at all — that clock
(contract_document_delivered_at) is the unbuilt half of the P4 compliance item above.
Q5 — must a gym-membership cancellation notice be in WRITING specifically, narrower than the
general continuous-transaction rule’s oral-at-the-business-or-by-phone allowance? §4’s own text
says “the member gives written notice”; §3 (the rule this document’s “What this means for Taikan”
section leans on for the one-month date) permits oral notice. The shipped channels include phone
and in_person as accepted notice channels — if §4’s stricter wording binds specifically to gyms,
those would need to always produce a written record (staff transcribing what was said) rather than
counting as notice in their own right.
Q6 — does the four-month window for a disabled, senior, or new-immigrant consumer (§2) reach a gym membership at all? §2 is framed around a distance or door-to-door SALE, and an online membership signup plausibly counts as one. If it does, Taikan needs to accept a status declaration (“one certificate, no further proof”) and honor a materially longer window than one month for these members — nothing in the shipped reason-code taxonomy or notice flow currently branches on this.
Q7 — do the §6 disclosure obligations bind Taikan as the platform, or only each org as the seller of record? Cancellation-channel details on every invoice/receipt/payment notice, and a direct cancellation link on the sales site’s home page (§3), are unbuilt either way: no such block exists on a generated receipt, and no per-org legal identity exists to print one (P4, above).
Q8 — does “the cancellation fee computes to zero” (above) hold for every plan configuration an
org can build today, or does a same-plan_group annual/monthly pair read as exactly the
committed-vs-alternative-track comparison §4’s fee formula assumes, even with no explicit
multi-month commitment field on either plan? Worth a second look once plan groups (FIT-289) are
in wider use — the zero-fee conclusion was reached against “no committed-term memberships” as a
product fact, and a same-offer cheaper-annual/pricier-monthly pair may or may not read as one to a
lawyer.
Q9 — may the cancel dialog show a “Talk to us first” button of equal weight beside the cancel action? §5 says the cancellation channels “may not be conditioned”. The proposed button records nothing on the subscription and the cancel action stays one tap away. Gates the P3 work; nothing of it is built yet.
Q10 — is a final pro-rata charge for the unpaid tail after notice permitted? §4 says the member
pays the proportional part up to the effective date; §3 says charging stops within 3 days. A monthly
member 20 days into a period gets an effective date 10 days past the paid period, and today that
tail is free. Decides the default of the P2 settle_unpaid_tail org setting.
Q11 — is the ID number required on a digital notice, or only permitted? §5 lets the business ask for name and ID and nothing more. Decides whether the planned field on the member cancel dialog is required when the compliance form is not prompted.
Q12 — §14ט online cancellation on the external billing tier. Already open in
docs/business/payments-terms-draft.md: the notice is accepted in-app and the org gets a task to
stop the standing order it alone controls. Is that sufficient when Taikan cannot stop the charge
itself?
Q13 — does the 14-day cooling-off apply after opening-day promotion of a presale, and what fee is allowed pre-start? A member promoted on opening day who cancels within 14 days of that first charge is inside the §1/§3 window but today gets the one-month notice with no refund. Decides the early-life immediate-end path and the presale pre-start fee cap (planned default zero).