Zodiac Tech CRM Docs

How billing & renewals work

Agreement states, automatic renewals, payment failures, grace and restriction — and why price and entitlements are deliberately separate.

everyone Medium ⏱ 12 min Updated 2026-10-05

How billing & renewals work

Commercial billing is the quiet machinery behind every workspace: an agreement is signed, a mandate is authorized with a payment provider, invoices are raised automatically on each renewal, and if a payment fails the workspace degrades gracefully — never instantly. This page follows that lifecycle and explains the one invariant that keeps everything sane: price and entitlements are separate.

The lifecycle in plain language

  1. Agreement. You agree commercial terms (price, billing cycle, what is included). The agreement moves through approval and payment-authorization states.
  2. Mandate. You authorize the payment provider (Razorpay or Cashfree) to debit on schedule. Once authorized, the agreement is ACTIVE.
  3. Renewal. On each billing date the CRM raises the renewal invoice, and the provider debits using the mandate.
  4. Failure path. A failed debit never locks you out immediately: the agreement moves to a grace period (default 7 days) with full access, then PAST_DUE, then RESTRICTED (premium features paused; data is never deleted; lead capture and read access continue), and only after 7 more days SUSPENDED (all access denied).
  5. Recovery. A successful payment at any point in the failure path returns the agreement to ACTIVE.
stateDiagram-v2
    [*] --> DRAFT
    DRAFT --> PENDING_CLIENT_APPROVAL
    PENDING_CLIENT_APPROVAL --> PENDING_PAYMENT_AUTHORIZATION
    PENDING_PAYMENT_AUTHORIZATION --> ACTIVE : mandate authorized
    ACTIVE --> PAYMENT_FAILED : debit failed
    PAYMENT_FAILED --> GRACE_PERIOD : automatic
    PAYMENT_FAILED --> ACTIVE : payment recovered
    GRACE_PERIOD --> PAST_DUE : grace expired
    GRACE_PERIOD --> ACTIVE : payment recovered
    PAST_DUE --> RESTRICTED : grace period elapsed
    PAST_DUE --> ACTIVE : payment recovered
    RESTRICTED --> SUSPENDED : 7 further days
    RESTRICTED --> ACTIVE : payment recovered
    SUSPENDED --> ACTIVE : payment recovered
    ACTIVE --> CANCELLED
    ACTIVE --> EXPIRED

What each degraded state allows

State Effect on the workspace
GRACE_PERIOD Full access while you fix the payment — nothing is paused
PAST_DUE Still working; the restriction clock is running
RESTRICTED Premium features paused. Allowed: crm, lead_management, followups, reports. Data is never deleted
SUSPENDED All access denied until payment is recovered

The invariant: price ≠ entitlements What you pay and what your workspace is allowed to use are stored and applied separately. Changing your price never silently changes your entitlements, and restoring access after a failure never requires renegotiating price. That separation is what makes the grace → restriction → suspension ladder predictable.

The automatic schedule

Job When What it does
CreateRenewalInvoice Daily 06:30 Raises the renewal invoice for ACTIVE agreements whose billing date has arrived
CheckUpcomingRenewals Daily 07:00 Flags renewals approaching so they can be actioned
CheckPaymentFailures Hourly Detects failed debits and elapsed deadlines
ApplyGracePeriod Daily 07:15 Moves PAYMENT_FAILED → GRACE_PERIOD and notifies the client
ApplyRestriction Daily 07:30 Moves PAST_DUE → RESTRICTED (after the grace period) and RESTRICTED → SUSPENDED (after 7 further days)
ReconcileProviderStatus + SyncAppliedChangeRequests Every 30 min Reconciles provider state and applies approved change requests

It all rides on cron These jobs are part of php artisan schedule:run — the single cron entry every install must have. A broken cron silently stops renewals, restrictions and recoveries. See Cron & queue workers.

Payments and mandates

  • Providers: Razorpay and Cashfree, configured by the platform at Super Admin → Billing → Providers & Settings (secrets encrypted).
  • Webhooks: /api/webhooks/{razorpay,cashfree} — signature-verified and idempotent (a replayed webhook cannot double-apply a payment).
  • Debit equals invoice: the provider debit, the renewal invoice, and the agreement's recurring gross amount (grossRecurringAmount()) are the same number by design — no drift between what is invoiced and what is debited.
  • Mandate changes: some changes (for example altering the mandate amount) require the client to authorize again — the change request honestly reports REQUIRES_NEW_AUTHORIZATION rather than pretending it applied.
  • Client surfaces: /admin/billing/agreement, /admin/billing/invoices, /admin/billing/payments show the agreement, invoices and payment history.

Troubleshooting notes

Symptom Likely cause Fix
Workspace restricted while a payment "went through" The webhook was never delivered (URL/secret), or cron is not running to recover state Check the provider webhook configuration and cron; reconciliation runs every 30 minutes
Renewal invoice missing Cron not running, or the agreement left ACTIVE Cron & queue workers; check agreement status
Client paid but still suspended Payment not matched to the invoice Reconciliation (ReconcileProviderStatus) usually fixes it within 30 minutes; check the payment reference
Change request stuck It requires new authorization from the client The request shows REQUIRES_NEW_AUTHORIZATION — the client must re-authorize
Grace period seems wrong Configurable platform setting Default is 7 days; platform admins can adjust it

Technical details

  • Tables: commercial tables (commercial_*) plus billing_invoices (not invoices); services in App\Services\CommercialBilling\*; providers in app/Billing/Commercial/*.
  • State machine: App\Services\CommercialBilling\AgreementStateMachine — allowed transitions are an explicit map; terminal states are CANCELLED and EXPIRED.
  • Entitlements: App\Services\CommercialBilling\EntitlementService — RESTRICTED allows crm, lead_management, followups, reports; SUSPENDED denies all; restriction application is best-effort after each transition (the state itself is the source of truth).
  • Jobs: App\Jobs\Billing\CreateRenewalInvoice, CheckUpcomingRenewals, CheckPaymentFailures, ApplyGracePeriod, ApplyRestriction (constant SUSPEND_AFTER_DAYS = 7), ReconcileProviderStatus, SyncAppliedChangeRequests — scheduled in routes/console.php.
  • Notification: BillingNotifier sends client-facing emails/in-app notices at grace, restriction and suspension.

Was this page helpful?