# cPanel cron jobs, queues, and recurring donation reminders

This guide covers the application's Laravel 13 / PHP 8.4 deployment on one cPanel shared-hosting account. It uses the database queue and short-lived cron workers, so Supervisor and Redis are unnecessary.

## 1. Confirm the hosting paths and capabilities

The examples use these placeholders. Replace them everywhere before running commands:

| Item | Example |
| --- | --- |
| cPanel username | `CPANEL_USER` |
| Laravel application root, containing `artisan` | `/home/CPANEL_USER/donation-site` |
| Domain document root | `/home/CPANEL_USER/donation-site/public` |
| PHP CLI executable | `/opt/cpanel/ea-php84/root/usr/bin/php` |

Keep the application root, `.env`, and logs outside the domain's public document root. The document root should point to Laravel's `public` directory.

Use cPanel Terminal or SSH to check the CLI PHP executable. The PHP version used by cron must match the website and satisfy the installed dependencies. Ask the host for its PHP path if the example does not exist. cPanel documents the full executable path for [running PHP commands in Terminal](https://support.cpanel.net/hc/en-us/articles/360050224413-How-to-run-PHP-commands-as-a-cPanel-user-via-Terminal).

```sh
/opt/cpanel/ea-php84/root/usr/bin/php -v
/opt/cpanel/ea-php84/root/usr/bin/php -m
command -v flock
command -v timeout
```

Confirm that:

- Cron jobs are available, preferably once per minute.
- CLI PHP has the application's required extensions, including the MySQL driver, and outbound HTTPS/SMTP access is allowed.
- `storage` and `bootstrap/cache` are writable by the account running PHP and cron.
- `/usr/bin/flock` and `/usr/bin/timeout` exist, or substitute the paths reported above. The cron commands below require them; ask the host if unavailable.
- PHP can launch the subprocesses used by Laravel's scheduler. A host that disables required process functions must enable them for CLI PHP.

`pcntl` allows Laravel to enforce individual job timeouts. Some shared hosts omit it. The queue cron below also uses the operating system's `timeout` to bound the entire worker process. `--max-time` alone checks between jobs and cannot interrupt a stuck network request.

## 2. Configure production delivery

Set these values in the production `.env`, preserving the application's existing key and database credentials:

```dotenv
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.example

QUEUE_CONNECTION=database
DB_QUEUE=default
DB_QUEUE_RETRY_AFTER=180
QUEUE_FAILED_DRIVER=database-uuids
CACHE_STORE=database

DONATION_CONFIRMATIONS_ENABLED=true
RECURRING_NOTIFICATIONS_ENABLED=true
RECURRING_GRACE_DAYS=7
RECURRING_MAX_MISSED_CYCLES=3
```

Recurring reminders are **disabled by default** in the repository. Enable `RECURRING_NOTIFICATIONS_ENABLED` on the host after configuring delivery. The grace-days default applies when creating commitments; existing commitments retain their saved grace periods.

In the admin panel:

1. **Settings → Branding:** save the site name used in SMS messages.
2. **Settings → Mail:** enable SMTP and save the complete host, port, encryption, username, password, sender address, and sender name. Use the provider's recommended values and send a test email. Saving SMTP settings in the admin panel also makes them available to Artisan workers.
3. **Settings → SMS:** enable SMS, select the provider, save its required credentials and sender configuration, and send a test SMS. Ensure the account supports Bangla Unicode messages and has sufficient credit.
4. Confirm the donor has a real email address and/or mobile number. The system uses saved donor contact details; gateway checkout defaults do not populate missing donor contacts.

If using environment-based mail configuration instead of the admin SMTP settings, configure `MAIL_MAILER=smtp`, `MAIL_SCHEME`, `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM_ADDRESS`, and `MAIL_FROM_NAME`. `smtp` with STARTTLS commonly uses port 587; `smtps` commonly uses port 465. Use the host's actual requirements. The default `MAIL_MAILER=log` writes email locally and does **not** deliver it.

From the application root, run the following with the selected PHP executable. Back up an existing production database before migrations:

```sh
cd /home/CPANEL_USER/donation-site
/opt/cpanel/ea-php84/root/usr/bin/php artisan migrate --force --no-interaction
/opt/cpanel/ea-php84/root/usr/bin/php artisan config:cache --no-interaction
/opt/cpanel/ea-php84/root/usr/bin/php artisan view:cache --no-interaction
/opt/cpanel/ea-php84/root/usr/bin/php artisan config:show donation --no-interaction
/opt/cpanel/ea-php84/root/usr/bin/php artisan schedule:list --timezone=Asia/Dhaka --no-interaction
```

The existing migrations provide `jobs`, `failed_jobs`, `cache`, and `cache_locks`; do not generate duplicate queue/cache migrations. The database cache is needed by the recurring task's `onOneServer()` and overlap locks. Refresh the config cache after subsequent `.env` changes.

## 3. Add two cron jobs in cPanel

Open **cPanel → Advanced → Cron Jobs**. Add two separate entries. Set **Minute, Hour, Day, Month, and Weekday** to `*` for both entries. Paste only the command into the Command field, without a leading `* * * * *`. cPanel supports [adding and editing cron jobs](https://support.cpanel.net/hc/en-us/articles/360053606793-How-to-create-or-edit-cron-jobs-in-the-cPanel-user-interface); use absolute executable and application paths.

### Cron job A: Laravel scheduler

```sh
/usr/bin/flock -n /home/CPANEL_USER/donation-site/storage/framework/scheduler-cron.lock /bin/sh -c 'cd /home/CPANEL_USER/donation-site && /opt/cpanel/ea-php84/root/usr/bin/php artisan schedule:run --no-interaction' >> /home/CPANEL_USER/donation-site/storage/logs/scheduler-cron.log 2>&1
```

This invokes Laravel's scheduler every minute. The application decides which tasks are due:

| Task | Current application schedule |
| --- | --- |
| `donation:process-recurring` | Daily at **00:15 UTC / 06:15 Asia/Dhaka** |
| `donations:recover-gateway-payments` | Every five minutes |

The application currently uses `UTC` in `config/app.php`; changing the server or cPanel clock does not change Laravel's configured timezone. Date-based recurring processing also currently uses the application's UTC date. `schedule:list --timezone=Asia/Dhaka` changes the displayed times only. If changing the reminder time later, review `routes/console.php` and the application's date handling together.

Laravel checks the current scheduled minute; it does not replay a daily task missed during downtime. After an outage, run the recurring command manually as described below. Do not add another daily cron for the same command alongside this scheduler.

### Cron job B: bounded database queue worker

```sh
/usr/bin/flock -n /home/CPANEL_USER/donation-site/storage/framework/queue-cron.lock /usr/bin/timeout 110 /opt/cpanel/ea-php84/root/usr/bin/php /home/CPANEL_USER/donation-site/artisan queue:work database --queue=default --stop-when-empty --max-time=50 --max-jobs=50 --sleep=1 --tries=3 --backoff=60 --timeout=45 --no-interaction >> /home/CPANEL_USER/donation-site/storage/logs/queue-cron.log 2>&1
```

This processes available notification jobs and exits when the queue is empty, the time budget is reached, or 50 jobs have been processed. Jobs remaining in the database wait for the next cron invocation. A worker already running holds the file lock, so another invocation skips it. Never delete a lock file while a worker is running; `flock` releases the lock when the process exits.

The worker normally starts once per minute. A slow batch may span a second cron invocation. Email and SMS each use a separate job. A queue that starts just before the scheduler enqueues reminders will process them on a later invocation.

The 180-second reservation (`DB_QUEUE_RETRY_AFTER`) exceeds both the 45-second notification timeout and the 110-second process limit. Keep this ordering if changing limits. The host must allow a cron process to run up to 110 seconds for these examples. If it imposes a lower limit, agree on compatible worker and provider-request limits before enabling delivery. Laravel describes [queue worker options and timeout rules](https://laravel.com/docs/13.x/queues#running-the-queue-worker).

The current broadcast implementation sends its selected audience inside one job. Large broadcasts can exceed these worker limits; use small recipient selections until broadcast delivery is split into smaller jobs. Inspect message logs before retrying a timed-out broadcast, since recipients already contacted can receive it again.

If the host only allows cron every five minutes, use `*/5` in the Minute field for both entries and keep the other fields `*`. This includes the recurring task's scheduled minute, but increases delivery latency. Avoid intervals that omit minute 15, or the daily task will not run. Do not run `queue:listen`, `schedule:work`, or an unbounded worker from this cron setup.

## 4. How recurring reminders behave

- Active commitments are processed on or after their next due date. Future, paused, and cancelled commitments do not generate reminders.
- The processor creates a pending donation cycle and queues one reminder per available channel for that due date. It reuses an existing cycle when rerun.
- Reminders use Bangla, even if the commitment was created while the site was in English. SMS includes the site name from Settings and omits the donation reference.
- Disabled reminders or phone-only donors with SMS disabled do not consume the notification marker. Enabling delivery allows an existing unnotified cycle to queue on the next processing run, provided it is still within its grace period.
- `last_notified_at` and `last_notified_due_date` mean the reminder was queued, not that it reached the donor. Delivery errors belong to the queue and message logs.
- On the due date plus the saved grace days, the pending cycle becomes failed; the processor advances the schedule. After the configured number of misses, it pauses the commitment. It does not send a reminder for a cycle already outside grace.
- Queue jobs recheck eligibility before sending. A paid or cancelled cycle, paused commitment, advanced schedule, expired grace period, or disabled reminder flag suppresses stale reminders.
- Notification jobs wait until a surrounding database transaction commits. Rollbacks discard the queued work. Failed deliveries get up to three attempts, with 60- and 300-second backoffs; cron timing can add further delay.
- These are reminders to donate, not automatic bank/card charges. Payment must still be completed through the application's existing donation workflow.

## 5. Verify the production setup

Use a donor and an active recurring commitment with contact details you control. Creating a commitment due today can immediately queue its first reminder; creating it with a future start date waits until it is due.

To process due commitments manually, run:

```sh
cd /home/CPANEL_USER/donation-site
/opt/cpanel/ea-php84/root/usr/bin/php artisan donation:process-recurring --no-interaction
```

This command changes due cycles and may queue real donor messages. Run it deliberately. Repeating it after successful dispatch should create no duplicate cycle or reminder for the same due date.

Wait for the queue cron, or run the **same bounded, locked queue command from section 3** in Terminal. It processes all eligible jobs on the `default` queue, including donation confirmations. Verify receipt of email/SMS rather than relying only on the command's success output.

Check:

- `storage/logs/scheduler-cron.log`: scheduler invocations and scheduled task results.
- `storage/logs/queue-cron.log`: processed and failed jobs, startup errors, and timeouts.
- `storage/logs/laravel.log` (or the configured logging channel): application exceptions.
- The admin message logs: SMS delivery attempts and provider errors. A successful provider response does not by itself prove handset delivery.
- `jobs` and `failed_jobs` in phpMyAdmin: queued work should drain and failures should be investigated. Empty tables alone do not prove that a reminder was generated.

To inspect failed jobs:

```sh
/opt/cpanel/ea-php84/root/usr/bin/php /home/CPANEL_USER/donation-site/artisan queue:failed --no-interaction
```

After fixing the specific provider or configuration problem, retry the affected job using its reported UUID:

```sh
/opt/cpanel/ea-php84/root/usr/bin/php /home/CPANEL_USER/donation-site/artisan queue:retry JOB_UUID --no-interaction
```

The next cron processes retried work. Do not routinely retry every failed job: an ambiguous timeout can occur after a provider accepted a message, so a retry may duplicate delivery. Old recurring reminders also remain subject to the eligibility checks above.

## 6. Troubleshooting and maintenance

| Symptom | Check |
| --- | --- |
| No reminder jobs | Confirm the recurring flag in `config:show donation`, a due active commitment, saved contacts, enabled SMS for phone-only donors, and a due date still within grace. Refresh config cache after changing `.env`. |
| Jobs remain queued | Check the queue cron, PHP path, database credentials, writable lock/log paths, host process limits, and `default` queue name. |
| Jobs fail immediately | Read the specific queue/application error; verify SMTP/provider credentials, outbound network access, PHP extensions, and sender approval. |
| Email job succeeds but nothing arrives | Ensure SMTP is enabled instead of `log`/`array`; check spam, sender-domain authentication, and the mail provider's delivery log. |
| Reminder does not run at midnight in Bangladesh | The current application schedule is 06:15 Bangladesh time. See section 3. |
| Recurring task remains locked after a crash | Confirm no scheduler task is still running, then use `artisan schedule:clear-cache --no-interaction`. This clears Laravel's scheduler locks; operating-system locks are separate. |
| Cron logs stop growing | Execute the exact cron command in Terminal and check cPanel cron access, paths, permissions, and overlapping/stuck processes. |
| Backlog grows every day | Check provider latency/rate limits and hosting CPU/process limits. Adjust the bounded batch only within those limits; sustained higher volume needs greater worker capacity. |

After deploying code or changing `.env`, rebuild the configuration cache. Cron workers start fresh each invocation; let an already running bounded worker exit before expecting it to use new code/settings. If a separate persistent worker is introduced later, restart it using Laravel's queue restart procedure.

Rotate or archive cron logs through the hosting provider's supported mechanism. Inspect failed jobs before optionally pruning old failures with `artisan queue:prune-failed --hours=168 --no-interaction`. Do not use `queue:clear` or `queue:flush` as routine maintenance; they delete pending work or failure history.

The local automated tests use in-memory mail and mocked SMS delivery. They verify processing and notification behavior, but cannot verify the production host's cron execution or external provider delivery. Complete section 5 after deployment.

## 7. Donor portal and OTP login

The donor portal is available through **Donor Login** in the public desktop navbar and mobile menu. It uses the public Blade layout and supports SMS or email OTP for existing donors. It does not create donor records during login.

Run the application's migrations after deploying this feature. The additive migration creates `donor_login_challenges`, adds donor verification/session metadata, and adds a donation reporting index. Existing donor and payment records are retained.

OTP messages are delivered immediately during the web request, rather than through the minute-based queue worker. Configure working SMTP and SMS settings before inviting donors. The `log` and `array` mail transports cannot deliver production OTP emails. SMS package logging is temporarily disabled for sensitive sends, and the application message log stores `[OTP redacted]` instead of the code. Test delivery using contact details you control.

Existing email addresses are initially ineligible for email login because older guest checkout could populate them without verification. A donor must first log in using SMS, then choose **Verify email address** on **My Profile** and enter the code sent to the saved address. Once verified, the donor can use either phone or email OTP. If the saved email is incorrect, staff must correct it through the existing donor administration screen. Changing the phone or email revokes previous donor sessions and outstanding challenges; changing the email also requires email verification again.

The existing scheduler cron automatically runs `donor:prune-login-challenges` daily at 00:00 UTC, deleting challenges expired more than one day ago. No additional cPanel cron entry is needed. Challenge expiry is checked during verification, so cleanup timing does not extend code validity. The configurable limits live in `config/donation.php` under `portal`.

PDF acknowledgements contain only canonical `paid`, non-deleted donations in the donor's selected Bangladesh payment-date range. Legacy `completed` records are retained and excluded from paid totals until reviewed. Downloads are private and generated on demand. Ensure `storage/app/private` is writable; mPDF creates its private temporary directory there. Large exports are limited by `donation.portal.pdf_max_rows` and request a shorter range instead of truncating the document.
