Skip to Content
Living documentation — last reviewed 2026-05-28
DecisionsADR-0021: An unpaid payment gets a grace period, then the plan is cancelled for non-payment

ADR-0021: An unpaid payment gets a grace period, then the plan is cancelled for non-payment

Status: Accepted, implemented. It replaced the “7 days booking → 14 days app-only → blocked” ladder that TKN-427 first drafted, before that ladder shipped. Date: 2026-10-03 Context owner: Saar Related: TKN-425, TKN-426, TKN-427 (TKN-433 is folded in) · payments behavior · subscriptions behavior


TL;DR

A missed payment gives the member a grace period, set per gym (default 7 days), during which they can still book. If the money has not arrived when the grace period ends, the plan is cancelled for non-payment. The debt stays open and the member stays in debt. The app stays usable, with no lock. Paying the whole debt later restores the plan for the period it covered, and billing continues on its normal date. One daily lifecycle job handles every transition driven by the calendar. Charge outcomes are handled where the charge happens.

Context

On 2026-10-02 Erez was told the rule was “1 week booking, 2 weeks app only, then blocked”. The code did none of that, and in some cases did worse:

  • A failed card renewal reached debt after about 10 days and was never cancelled.
  • An expired card raised a Sentry alert and nothing else.
  • Pay-later terms were never ended.
  • The daily runExpiredSubscriptions job (10:00 UTC) silently cancelled any active plan whose period had ended. That included renewing plans whose charge was blocked, held or timed out, cash plans whose prepaid coverage ran out, and pay-later terms still owing money. These cancellations had no reason, no audit row, no event and no notice.

Decision

Day 0: a payment is missed

Day 0 is whichever of these applies:

  • A card renewal is declined, or the card is expired or missing when the charge is attempted.
  • A presale’s first charge (on its start or opening day) is declined, or the card is expired or missing. The presale starts on that day as if it had been paid and enters the grace period. The amount owed is the agreed presale price.
  • A cash monthly plan reaches the end of its prepaid period with no renewal payment recorded.
  • A pay-later term passes its due date. The due date is the start date plus the staff-set grace, 7 days by default, counted in org calendar days.

Grace period: days 0 to N

N is a per-gym setting, “Days to pay before the plan is cancelled”. The default is 7 and the allowed range is 0 to 30. Staff set it in Settings → Booking.

  • The plan stays active and the member can book.
  • The member’s status is in debt.
  • App, web, email and push all say: “You owe ₪X. Pay by {date} or your plan will be cancelled.” Staff see the same status and date.
  • Card retries run halfway through the period and again the day before cancellation. With the default N = 7 that is day 3 and day 6. A period shorter than 2 days gets one retry.
  • Any payment in this window clears the debt and the plan simply continues.
  • The clock does not run while a plan is frozen (paused).
  • Pay-later: the grace staff set on the term still decides its due date. The gym’s N counts from that due date.

Day N, still unpaid: cancelled for non-payment

  • The cancellation records a reason, an audit row and the cancellation event.
  • Future bookings are removed and new bookings are refused.
  • The debt stays open: the full missed period at the agreed price. The member stays in debt.
  • The app stays usable. It shows “Plan cancelled for non-payment. You owe ₪X.” with options to pay and to contact the gym. Access ends only if the owner removes the member.

Paying after the cancellation

  • Only the full debt is accepted.
  • Payment clears the debt and restores the plan for the period the debt covered. Example: a November debt paid on 20 Nov restores 1–30 Nov.
  • The next charge stays on the normal billing date (1 Dec in that example).

Amounts

Always the price actually agreed: a negotiated or special price, the intro-period price, a discount, or the agreed pay-later amount. Never the list price.

Failures on our side start no clock

  • Cardcom down.
  • The terminal busy.
  • A timeout with an unknown outcome.
  • A card saved on a different Cardcom terminal.
  • No payment provider configured.

In all these cases the plan stays active until the charge resolves. Nothing is cancelled.

One daily lifecycle job owns the calendar-driven transitions

It replaces runExpiredSubscriptions and moves out of NotificationSchedulerService. It runs hourly and acts on each gym once that gym’s local calendar day has changed.

TransitionOwner
Pack or drop-in validity ran out, fixed term ended, a set-period gift ended (₪0, reason “gift ended”)Lifecycle job. Ends the plan with a reason, an audit row and the cancellation event. No debt is created.
A card renewal is declined, or the card is expired or missingThe charge jobs (02:00 renewal, the minute-level catch-up, the webhook). They set in debt the moment the charge fails.
Cash coverage ended with no renewal paymentLifecycle job sets in debt. The clock starts.
Pay-later due date passedLifecycle job sets in debt. The clock starts.
Day N passed, still unpaidLifecycle job cancels the plan for non-payment. The debt stays open.
Grace countdown notices (email and push)Lifecycle job
Debt paidThe payment handlers clear the debt and restore the plan immediately

The lifecycle job never ends a plan that renews and has a charge pending or held.

Presales

  • A presale cannot book before its start date.
  • A failed first charge follows the same rule as a failed renewal. The plan starts, the member is in debt and can book for N days, and retries run halfway through and on the day before cancellation. On day N the plan is cancelled for non-payment.
  • A first-charge failure also opens a retention case on day 0, so staff can call the new member personally rather than leave it to automated notices.
  • A capped presale keeps its seat during the grace period.

Members already behind

None exist in production, so no transition rule is needed.

Consequences

  • State mapping. In debt (grace) is subscriptions.status = 'debt' with debt_amount_in_cents, debt_since (day 0) and debt_reason (charge_failed, card_unusable, cash_lapsed, payment_due); next_charge_date holds the next retry. Cancelled for non-payment is status = 'cancelled' with cancellation_reason = 'non_payment', the debt columns kept and the membership still debt. past_due is no longer written by any billing path; the enum value and its readers stay as legacy.
  • One setting. organizations.billing_grace_days (0–30, default 7, “Days to pay before the plan is cancelled”) replaces the ladder’s two rung settings and its activation instant. plan_required_to_book stays. Migration 0164_special_meteorite also adds organizations.billing_lifecycle_day, subscriptions.debt_reason, subscriptions.debt_notice_stage and subscriptions.payment_last_day.
  • Retries. At org-local midnight of day ⌊N/2⌋ and day N−1 (one retry halfway for N = 1, none for N = 0), always for the amount captured on day 0. A declined retry never adds to the debt.
  • Day 0 grants the period on credit. The missed period is written on day 0 (credits refilled, the intro cycle it is priced at consumed); paying never re-anchors it, and the next charge stays on current_period_end.
  • Lifecycle job. SubscriptionLifecycleService.tick runs hourly and does one pass per org-local day. It replaces runExpiredSubscriptions and the ladder’s phase-notice job; card-expiry reminders (14 / 7 / 1 days) are kept. ₪0 open-ended plans now roll into their next period instead of being cancelled, and a paid recurring card plan with no card and no renewal ends as term_ended.
  • Paying. Members pay a card debt themselves through POST …/subscriptions/my/:id/pay-debt (full amount only); staff use clear-debt, the desk records cash for a lapsed cash plan, and settling a pay-later term clears its debt. All of them restore a plan cancelled for non-payment.
  • Notices. Debt opened (day 0), last day to pay, and cancelled, each once per debt (debt_notice_stage), by email and push under the billing preference.
  • Members list. A member whose plan was cancelled for non-payment with debt still open is in the non_payment member state; a plan in grace keeps the member training with the pay-by date.
  • Presales. A failed first charge starts the presale that day in debt and opens a presale_charge_failed retention case; the old 3-retries-then-cancel presale path and its card-recovery promise are removed.
  • Automations. SUBSCRIPTION_DEBT_OPENED now fires on day 0 rather than on a third decline, and MEMBERSHIP_CHURNED fires for a non-payment cancellation.
  • Contract: payments › Unpaid payment, subscriptions › Subscription lifecycle, scheduling › Billing gate.