Skip to content

Getting started

Scheduled jobs

These console commands need to run on a schedule. Every one is idempotent and safe to re-run, and each supports --dry-run (except app:admin:create-user, which is manual, app:shifts:generate and app:webhooks:retry).

CommandSuggested scheduleWhat happens if it never runs
app:expire-delivery-offersevery minuteA rider who ignores an offer leaves the job stuck: nobody else is ever offered it.
app:dispatch-scheduled-ordersevery 5 minutesOrders placed for a later time are never released into dispatch.
app:orders:expire-unpaidevery 15 minutesAbandoned checkouts stay pending forever and keep the customer credit they were going to use.
app:webhooks:retryevery minutePartner webhooks that failed once (5xx, timeout) are never sent again.
app:shifts:tickevery 5 minutesCouriers aren't reminded before their shifts, and check-ins and no-shows aren't recorded (going online still checks a courier in).
app:shifts:generatedaily (just after midnight)Couriers can't book shifts for new days; only the days opened when a shift pattern was last saved stay bookable.
app:generate-payouts --payee=riderweeklyRiders are never paid; their wallet balances grow forever.
app:generate-payouts --payee=vendor1st and 16th of the monthVendors are never paid.
app:kyc:check-expirydailyCouriers are never warned before a document expires and keep working with expired documents.
app:account:process-deletionsdailyAccounts whose deletion was requested are never anonymised.
app:messages:purgedailyChats between customers and couriers, and inbox notifications, are kept forever instead of chat.retention_days / notifications.retention_days.
app:auth:purgedailyUsed sign-in codes (phone and new-device) and expired verified devices pile up in the database.

Crontab

* * * * *   cd /path/to/app && php bin/console app:expire-delivery-offers   >> var/log/cron.log 2>&1
*/5 * * * * cd /path/to/app && php bin/console app:dispatch-scheduled-orders >> var/log/cron.log 2>&1
*/15 * * * * cd /path/to/app && php bin/console app:orders:expire-unpaid >> var/log/cron.log 2>&1
* * * * *   cd /path/to/app && php bin/console app:webhooks:retry >> var/log/cron.log 2>&1
*/5 * * * * cd /path/to/app && php bin/console app:shifts:tick >> var/log/cron.log 2>&1
15 0 * * *  cd /path/to/app && php bin/console app:shifts:generate >> var/log/cron.log 2>&1
0 3 * * 1   cd /path/to/app && php bin/console app:generate-payouts --payee=rider --days=7   >> var/log/cron.log 2>&1
0 3 1,16 * * cd /path/to/app && php bin/console app:generate-payouts --payee=vendor --days=15 >> var/log/cron.log 2>&1
0 6 * * *   cd /path/to/app && php bin/console app:kyc:check-expiry >> var/log/cron.log 2>&1
30 3 * * *  cd /path/to/app && php bin/console app:account:process-deletions >> var/log/cron.log 2>&1
45 3 * * *  cd /path/to/app && php bin/console app:messages:purge >> var/log/cron.log 2>&1
50 3 * * *  cd /path/to/app && php bin/console app:auth:purge >> var/log/cron.log 2>&1

Payouts

Customers always pay the platform's Stripe account; each payout run pays a rider or vendor their net ledger balance (earnings minus refunds and any cash-on-delivery money they still hold), so the platform's commission is simply what is never paid out. A run reserves the money off the wallet first, so a second run can never pay the same earnings twice.

The rail is chosen per payee type in Settings → Payments (payments.payouts.rider_method, payments.payouts.vendor_method):

  • bank_transfer (default): riders and vendors enter their IBAN in the app (Profile → Payouts) and an admin verifies it in Wallets → Verify bank details; any later change goes back to review. Runs create pending payouts. In Payouts, use Export pending bank transfers to get one CSV for the bank (use each payout reference as the transfer description), pay it, then select the payouts and Mark paid. Cancel & return funds puts a payout's money back for the next run.
  • stripe_connect: payees open Stripe's hosted onboarding from the app and get an Express account. Runs transfer straight from the platform balance; a failed transfer returns the money to the wallet and is retried on the next run. To enable it:
    1. Activate Connect in the Stripe dashboard (Settings → Connect).
    2. Set platform.public_url; Stripe returns payees to <public_url>/payouts/stripe/return.
    3. Add a webhook endpoint for events on connected accounts pointing at <public_url>/api/v1/webhook/stripe with account.updated, and put its signing secret in payments.stripe.connect_webhook_secret.
    4. Keep enough available balance for transfers: set the platform's own Stripe payout schedule to manual (or a long delay), otherwise Stripe sweeps the balance and transfers fail with insufficient funds.

Wallets without verified bank details, or without an active Stripe account, are skipped and paid on a later run. Payees get a push notification when a payout is sent.

Background queue (optional)

Email, push notifications and realtime broadcasts are dispatched through Messenger. The transport is MESSENGER_TRANSPORT_DSN, which defaults to:

MESSENGER_TRANSPORT_DSN=sync://

sync:// handles each message in-process, so no worker is required and the default deployment has nothing extra to supervise. To move that work into the background:

MESSENGER_TRANSPORT_DSN=doctrine://default

then run a worker (the transport creates its own messenger_messages table on first use):

php bin/console messenger:consume async --time-limit=3600

Supervise it so it restarts after the time limit, with systemd, supervisor, or whatever the deployment already uses.

Rider location updates are deliberately never queued. A position is worthless once stale, and they arrive roughly every 15s per tracked rider, so queueing them would trade freshness for volume. They broadcast synchronously, best-effort, throttled by commission.rider_location_broadcast_seconds.

Found a mistake? Tell us.