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) is the basis of the table below.


What Taikan sells, and which regime each falls under

ProductRegimeConsequence
Rolling monthly subscriptionעסקה מתמשכת — continuousCancellable any time; charging stops within 3 days of notice; gym rule adds a one-month effective date (see Q1)
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 cancellation_settlements table. This was the largest single item in the earlier plan and it is gone on the merits, not merely deferred.


Code state

Fixed in this branch (feat/cardcom-followups):

WasNow
Charge cron had no cancel_at_period_end filter, so it charged members who had cancelled and pushed current_period_end forward — losing the cancellation permanently, every intervalExcluded in the due query and re-checked under the row lock; regression-tested across both crons
A pending cancellation request did not stop billingExcluded in the due query too — the notice is the notice, regardless of whether staff got to it
Cancelling required status === 'active'Any live status can cancel; only an already-cancelled subscription is refused
A scheduled plan change blocked cancellation with a 409Cancellation supersedes and clears the schedule
Reason was mandatory (API + UI)Optional everywhere, label marked optional in en/he/ru
Tranzila’s refund() fabricated a trz_ref_<uuid> and reported successThrows; unreachable today (getRefundCapability() === 'manual') but no longer a landmine

Still open:

#GapWhereSize
1No cancellation effective date. Everything keys off current_period_end, a billing artefact. Neither “3 days from notice” nor “one month from notice” is representable.subscriptions.service.ts sweep + schemaBlocked on Q1
2No notice record. We store cancellation_requested_at (an app event) but not the channel or a delivered-at date. Phone/email/in-person notices have no intake at all.schema + a staff intakeMedium
3No 14-day cooling-off for punch cards and drop-ins, and no contract_document_delivered_at to start the clock lawfully.plans purchase flowMedium
4No pro-rata refund on cancellation. applyImmediateCancel cancels and returns nothing.subscriptions.service.ts:1128+Small once Q1 lands
5Manual refund tasks have a 3-day due date nothing enforces, and no refund SLA exists.payment.service.ts:619+Small
6refund() resolves the adapter from the org’s current provider, not txn.provider — a refund after a gateway switch calls the wrong PSP.payment.service.ts:454Small
7Two silent paths: staff immediate cancel and automatic refunds notify the member of nothing.subscriptions.service.ts:1128+, payment.service.ts:507+Small
8No ID number captured on a cancellation notice — the one identifier we are actually entitled to ask for.schema + UISmall
9No country / legal-entity-name / ח.פ on organizations, so the required business-identity and cancellation-channel disclosure cannot be rendered on any invoice or receipt.schema + disclosure blockMedium
10No signed-out cancellation route, and no prominent home-page link on minisites. A member who cannot sign in cannot give notice.minisites + a public endpointMedium
11Pending/incomplete payments cannot be revoked from the dashboard — abandoned checkouts sit as pending rows forever.payments dashboardIn progress

Open 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 only thing blocking the effective-date work, and it is a lawyer question, not an engineering one. Design accordingly: put the rule in one pure function (computeCancellationEffectiveAt) over an explicit stored notice date, so the answer changes one function rather than the schema.

Q2 — 3 days or 3 business days? The guidance says 3 days; the statute reportedly says three business days. Implement 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.