Skip to Content
Living documentation — last reviewed 2026-05-28
ScratchCancellations & refunds — where we are and what's left

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

ProductRegimeConsequence
Rolling monthly subscriptionעסקה מתמשכת — continuousCancellable 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 purchase14 days from purchase or the written document, whichever is later
Drop-in (single session)Not continuous14 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:

WasNow
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/refundedFetches 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 transactionResolves 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 landWHERE … 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 backAccepts 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 itExtracted 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 limboDaily 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 a contract_document_delivered_at timestamp 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).