> ## Documentation Index
> Fetch the complete documentation index at: https://docs.subscribd.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Dunning

> Automatically retry failed payments and manage past-due subscriptions

# Dunning

When an invoice payment fails, Subscribd automatically retries the charge according to a configurable dunning schedule. Subscriptions that exhaust all retries without a successful payment are marked `expired` and access is revoked.

## Default retry schedule

Out of the box, Subscribd retries failed payments at:

* **24 hours** after initial failure
* **72 hours** after initial failure
* **168 hours** (7 days) after initial failure

If all three retries fail, the subscription moves to `expired` and the `SubscriptionExpired` event is fired.

## Configuration

```php theme={null}
// config/subscribd.php
'dunning' => [
    'strategy'       => \Pixelworxio\Subscribd\Dunning\ConfigDrivenDunningStrategy::class,
    'retry_after'    => [24, 72, 168],   // hours after initial failure
    'notify_at'      => [72, 120],       // hours — fires PaymentRetryFailed at these points
    'grace_days'     => env('SUBSCRIBD_GRACE_PERIOD_DAYS', 3),
    'cancel_on_fail' => env('SUBSCRIBD_CANCEL_ON_DUNNING_FAIL', true),
],
```

The `retry_after` array defines when retries are attempted, in hours after the first failure. You can add or remove entries to suit your billing cycle. For example, a weekly billing cycle might use `[24, 96, 240]`.

## How dunning works

1. **Payment fails** — gateway fires `invoice.payment_failed` (or equivalent). The subscription moves to `past_due`.
2. **Retry job scheduled** — a `RetryInvoice` job is queued for each interval in `retry_after`.
3. **Retry succeeds** — the invoice is marked `paid`, the subscription returns to `active`, and `InvoicePaid` is fired.
4. **Retry fails** — `PaymentRetryFailed` is fired if the current hour matches a `notify_at` threshold.
5. **All retries exhausted** — if `cancel_on_fail` is `true` (default), the subscription is marked `expired` and `SubscriptionExpired` is fired.

## Custom dunning strategy

Implement `Pixelworxio\Subscribd\Contracts\DunningStrategy` to replace the built-in schedule with custom logic:

```php theme={null}
<?php

namespace App\Billing;

use Pixelworxio\Subscribd\Contracts\DunningStrategy;
use Pixelworxio\Subscribd\Models\Invoice;

class MyDunningStrategy implements DunningStrategy
{
    /**
     * Return retry delay in hours for the given attempt number (1-indexed).
     * Return null to stop retrying.
     */
    public function retryAfterHours(Invoice $invoice, int $attempt): ?int
    {
        return match ($attempt) {
            1 => 24,
            2 => 72,
            3 => 168,
            default => null,
        };
    }

    public function shouldCancelOnFailure(Invoice $invoice): bool
    {
        return true;
    }
}
```

Register your strategy in `config/subscribd.php`:

```php theme={null}
'dunning' => [
    'strategy' => \App\Billing\MyDunningStrategy::class,
],
```

## Notifying customers during dunning

Listen to `PaymentRetryFailed` to send reminder emails or in-app notifications:

```php theme={null}
use Pixelworxio\Subscribd\Events\PaymentRetryFailed;

class SendPaymentRetryFailedNotification
{
    public function handle(PaymentRetryFailed $event): void
    {
        $event->subscription->billable->notify(new PaymentActionRequiredNotification($event->invoice));
    }
}
```

Register your listener in `AppServiceProvider::boot()`:

```php theme={null}
Event::listen(PaymentRetryFailed::class, SendPaymentRetryFailedNotification::class);
```

## Manually retrying an invoice

Retry a specific invoice immediately, bypassing the dunning schedule:

```php theme={null}
use Pixelworxio\Subscribd\Actions\RetryInvoice;

app(RetryInvoice::class)->execute($invoice);
```

This attempts a charge on the customer's default payment method and returns the updated `Invoice` model. If the charge succeeds, the subscription returns to `active`.

## Grace period

When a subscription enters `past_due`, the billable retains access for `grace_days` (default: `3`) before the subscription is expired. Configure this globally or per plan:

```env theme={null}
SUBSCRIBD_GRACE_PERIOD_DAYS=5
```

Or in `config/subscribd.php`:

```php theme={null}
'dunning' => [
    'grace_days' => 5,
],
```

## Updating payment methods

When a customer updates their payment method during a `past_due` period, trigger an immediate retry:

```php theme={null}
use Pixelworxio\Subscribd\Actions\RetryInvoice;

// After payment method updated — surface the failed invoice and retry
$invoice = $user->invoices()->failed()->latest()->first();

if ($invoice) {
    app(RetryInvoice::class)->execute($invoice);
}
```

## Checking dunning status

```php theme={null}
$user->subscription()->status->value; // 'past_due'

$user->pastDue();   // true while in dunning
$user->subscribed(); // true until grace_days expires
```

## Next steps

* **[Hooks and Events](/advanced/hooks-and-events)** — Events fired during the dunning cycle
* **[Webhooks](/advanced/webhooks)** — Receiving payment failure events from gateways
