Zodiac Tech CRM Docs

Payment failures & restrictions

What happens when a payment fails — the grace and restriction ladder, what stays open at each stage, and how recovery works.

super-admin support Medium ⏱ 10 min Updated 2026-10-05

Payment failures & restrictions

When a client's AutoPay payment fails, the platform does not cut them off instantly. It walks a ladder: friendly → firm → blocked, with time (and clear messages) at every step. This page explains the ladder, exactly what a client can still do at each stage, and how they get restored.

Who this is for

Super Admins and support staff handling a client whose payment failed.

  • Difficulty: Medium
  • Time: 10 min
  • Prerequisites: A configured provider (Payment providers) and the grace period setting on that page.

The ladder

Active → Payment failed → Grace period → Past due → Restricted → Suspended
                (hours)      (default 7 days)   (7 more days)  (7 more days)
Stage Triggered by What the client sees
Payment failed A provider payment.failed event, or a revoked mandate Failure message; retry is still possible
Grace period The grace job (ApplyGracePeriod, daily 07:15) after a failed payment Reminder messaging; everything still works
Past due The catch-up sweep when the grace window is exhausted Stronger reminder; still no restriction yet
Restricted ApplyRestriction (daily 07:30) once Past due is older than the grace period Only four core features remain
Suspended The same job once Restricted is older than 7 days (SUSPEND_AFTER_DAYS) Every feature denied

Times are configurable: the grace period (default 7 days, max 90) is set at Billing → Providers & Settings. The 7-day Restricted-to-Suspended window is fixed.

What "Restricted" actually leaves open RESTRICTED allows only: CRM, lead management, follow-ups and reports. Everything else — Meta, WhatsApp, automations, AI, mobile, API — is denied, with the message: "This feature is not included in your current service agreement. Please contact your account manager."

SUSPENDED denies everything — every feature check returns false.

Two different "suspended" ideas — don't mix them up A commercial Suspended agreement blocks features through entitlements. A manually suspended workspace (the Suspend button under Businesses) sets the workspace inactive and bounces its users to /admin/subscription-required. They are independent mechanisms — a workspace can be active while its agreement is Restricted, and vice versa.

Step 1 — See a failure early

Failures surface in three places:

  1. Billing → Commercial Billing (/super-admin/billing/commercial) — the Payment failures / Past due / Grace / Restricted / Suspended cards.
  2. The payment-failure job runs hourly: it finds overdue invoices, notifies the client once (deduplicated, so no spam), marks invoices Overdue, and advances the state ladder based on how old the failure is.
  3. Reconciliation alerts (every 30 minutes) — when the provider's record and the platform's record disagree, an alert is raised for a human instead of silently editing financial data. Providers unreachable or an unknown provider status also raise alerts. When the provider later confirms the local state, the alert is resolved.

Step 2 — Contact and retry

Before the ladder bites, the best fix is a retry or a manual payment:

  • The client's successfully authorized mandate can be charged again by the provider; the platform books the payment when the webhook arrives.
  • Offline arrangements: Record Manual Payment on the agreement (bank transfer / UPI / cheque with a reference).

Step 3 — Recovery

Recovery is driven by the provider confirming the subscription is healthy again:

  1. The client re-authorizes / the provider retries and succeeds.
  2. The platform verifies with the provider — a verified authorization (BillingService::verifyAuthorization()) or a subscription.authorized webhook moves the agreement back to Active from any recoverable state.
  3. Every entitlement check re-reads the active agreement, so access is restored immediately — no cache to clear.

If instead the provider and local records disagree, the reconciliation alert stays open until a human resolves it. Never "fix" a mismatch by editing payment rows.

Expected result

Failures are noticed the day they happen, the client hears from you before restrictions hit, and a client who pays is back to full service the moment the provider confirms.

Common mistakes

  • Restoring a client by editing their workspace/plan instead of the agreement. Entitlements come from the agreement; payment restores the agreement, not the schedule row.
  • Assuming Restricted = no login. It is a feature-level squeeze, not a login block.
  • Setting the grace period to 0 or 1. That escalates to restriction almost immediately — use the default 7 unless the contract says otherwise.
  • Ignoring reconciliation alerts. They exist precisely because money must not be guessed at.
  • Telling the client "you'll be cut off today" at Payment failed stage. The ladder buys time; communicate the actual stage.

Troubleshooting

Symptom What to check
Client paid but still Restricted Was the authorization verified with the provider? Check the agreement Events/Timeline; provider confirmation restores Active
No overdue notifications sent Dedup may have already sent one for that invoice; check the client's notifications
Ladder not advancing Cron: the grace/restriction jobs run daily at 07:15 / 07:30 — see Queues, jobs & scheduler
Reconciliation alert stuck open Provider still shows a mismatch; resolve at the provider, then the next pass closes it
Client claims they never gave AutoPay consent Check the mandate row and the agreement's authorization history

Technical details

  • Ladder jobs: App\Jobs\Billing\CheckPaymentFailures (hourly, no-overlap 20 min), ApplyGracePeriod (daily 07:15), ApplyRestriction (daily 07:30, SUSPEND_AFTER_DAYS = 7).
  • Statuses: App\Models\CommercialAgreement — PAYMENT_FAILED, GRACE_PERIOD, PAST_DUE, RESTRICTED, SUSPENDED; transitions in App\Services\CommercialBilling\AgreementStateMachine.
  • Entitlements: EntitlementService — RESTRICTED_ALLOWED_FEATURES = ['crm','lead_management','followups','reports']; SUSPENDED denies all.
  • Recovery: BillingService::verifyAuthorization() + CommercialAgreementService::markActivated() (idempotent, allowed from recoverable states).
  • Reconciliation: App\Services\CommercialBilling\BillingReconciliationService, ReconcileProviderStatus job every 30 min; alerts in billing_reconciliation_alerts.
  • Notifications: BillingNotifier::paymentFailed / gracePeriodStarted / accountRestricted / accountSuspended.

Was this page helpful?