Skip to content

Artisan Command Reference

Yaya Engine ships a large custom Artisan surface split across five purpose groups: Rada weather ingestion and load-testing, one-off Supabase backfills tied to the Rada v2 cutover, Telegram webhook and broadcast management, MCP Passport client provisioning, and locale/admin housekeeping. Most of the backfill and Rada commands are Laravel ports of scripts that used to run directly against Supabase before Rada v2 moved that data into yaya-engine's own Postgres; the rest are the scheduled jobs (routes/console.php) that keep alerts, notifications, and message batches flowing without manual intervention. This page lists every command's signature and what it actually does; for how to invoke the Rada-facing ones against a live environment see Run the cross-repo harness.

Rada weather and load-testing

rada-weather:ingest-woredas and rada-weather:update-thresholds are direct Laravel ports of Python scripts that used to run against Supabase (ingest_ethiopia_woredas.py / update_ethiopia_thresholds.py). Both read a 10-column CSV by position, not by header nametown_id, country, country_name, state, town, latitude, longitude, yellow, orange, red — because matching by position survives spreadsheet exports that rename headers [@ingest-woredas] [@update-thresholds]. ingest-woredas upserts both towns (keyed on country, state, town) and states (keyed on country, state), deriving each state's thresholds as the max of its woredas' thresholds so a state-level alert never fires when no woreda within it would [@ingest-woredas]. update-thresholds only touches the yellow/orange/red columns on existing towns — matching is case-insensitive on state/town so an operator's spreadsheet casing doesn't need to match the database exactly — and requires --force to skip a confirmation prompt outside of --dry-run [@update-thresholds].

Command Purpose
rada-weather:ingest-woredas {source} {--dry-run} {--country=ET} Ingest ADM1 (states) + ADM3 (towns) geography and 24h rain thresholds from CSV [@ingest-woredas]
rada-weather:update-thresholds {source} {--dry-run} {--force} {--country=ET} Update thresholds only on existing towns from CSV; does not touch identity or lat/lon [@update-thresholds]
rada:loadtest:composer-bulk-insert {--base-url=} {--token=} {--rows=21000} {--chunk=5000} {--alert-id=999999} {--country=Colombia} {--sla-seconds=1800} {--timeout=120} Drives the live /api/v1/composed-messages/bulk endpoint with composer-shaped payloads to verify the 21,000-rows-per-30-minute SLA the Python composer requires. Does not clean up the rows it inserts — the operator deletes them by the supplied --alert-id, or runs against a disposable environment [@composer-bulk-insert]
rada:prune-forecast-store {--days=90} {--dry-run} Deletes superseded (non-latest) town_forecasts/town_observations rows older than the retention window; the latest row per group is always kept. Scheduled daily at 01:00 [@prune-forecast-store] [@console-schedule]

rada:loadtest:composer-bulk-insert is deliberately different from the Pest harness in tests/Performance/: it issues real HTTP calls, so it measures the full network + serialization + Postgres stack rather than an in-process benchmark [@composer-bulk-insert].

Supabase backfills (Rada v2 cutover)

These commands move data that used to live only in Supabase into yaya-engine's own tables, and are designed to be safely re-run: every one is idempotent, upserting on the same unique key the destination table's migration defines, so a repeated run converges instead of duplicating rows.

Command Purpose
rada:backfill-from-supabase {--dry-run} {--table=} {--batch-size=500} {--page-size=5000} {--max-orphan-ratio=} {--resume-from-id=} Backfills user_nearest_townuser_nearest_towns and templatesmeta_templates from Supabase PostgREST. Keyset-paginated per table — user_nearest_town's key is the uuid user_id column, not id, because that table has no id column; keying on the wrong column made PostgREST 42703 and silently copy zero rows (ye#382) [@backfill-from-supabase]
rada:backfill-geography {--table=both\|towns\|states} {--dry-run} {--batch-size=500} {--page-size=1000} {--resume-from-id=} Backfills towns_24h_rain_thresholds + states_24h_rain_thresholds into local towns/states. Maps Supabase's full country name to ISO-2 via a small built-in lookup (EthiopiaET, KenyaKE, ColombiaCO); unknown countries are skipped and logged rather than crashing the run [@backfill-geography]
rada:backfill-user-country {--dry-run} {--chunk=500} Backfills blank users.country from the user's FSP country, reusing User::inheritCountryFromFsp() — the same rule the registration hook applies — so self-serve signups (Telegram, OTP) that never collected a country stop reporting zero in country-scoped dashboards. Dumps a before-state CSV to storage/app/backfill-user-country/ before writing, so a bad run is reversible [@backfill-user-country]
supabase:sync-users {--dry-run} Syncs users without a supabase_user_id to the legacy Supabase database; no-ops if SUPABASE_SYNC_ENABLED=false [@sync-users-to-supabase]
supabase:sync-missing-alert-subscriptions {--dry-run} Backfills alert subscriptions in Supabase for users who have a confirmed location but no active subscription there [@sync-missing-alert-subscriptions]

Both rada:backfill-from-supabase and rada:backfill-geography guard on an orphan-failure ratio: if more than 5% of processed rows can't be matched to a yaya-engine record (a missing user, or an unmapped country) after more than 100 rows are processed, the run aborts rather than silently dropping a large fraction of data, and reports the last committed id so the operator can resume with --resume-from-id [@backfill-from-supabase] [@backfill-geography].

Telegram

Command Purpose
telegram:set-webhook Registers the Telegram webhook URL with Telegram [@telegram-set-webhook]
telegram:unset-webhook {--drop-pending} Removes the webhook (rollback or a switch to polling) [@telegram-unset-webhook]
telegram:smoke {--base=} Smoke-tests the webhook endpoint: expects 404 with bad secrets, 200 with the configured ones [@telegram-smoke]
telegram:broadcast-location-request {--dry-run} {--variant=generic\|flood} One-off backfill: dispatches a location-share prompt to existing Telegram bot users who never confirmed a location. --variant=flood sends the Ethiopia flood/rain Amharic copy instead of the generic copy, tracked on a separate "prompted once" column per variant so the two campaigns never collide [@telegram-broadcast-location-request]
telegram:request-phone Sends a Telegram message with a Mini-App button that deep-links to /payout/phone [@telegram-request-phone]
telegram:send-phone-reminders Dispatches one-time phone-number reminders to users registered 48h+ ago with a linked Telegram account but no phone. Scheduled hourly [@telegram-send-phone-reminders] [@console-schedule]
telegram:send-alert-phone-followups 30 minutes after an alert event, re-prompts Telegram recipients who still have no phone number; idempotent catch-up over the last 24h. Scheduled every 10 minutes [@telegram-send-alert-phone-followups] [@console-schedule]

telegram:broadcast-location-request's variant selection is a command flag, never derived from the user's locale — the flood copy is an Ethiopia campaign, not an Amharic-locale rule [@telegram-broadcast-location-request].

MCP client provisioning

Command Purpose
mcp:ensure-personal-access-client Idempotently ensures a Passport personal-access grant client exists for the admin_users provider — the client MCP token minting uses to issue tokens for InternalYayaServer callers. Safe to re-run: reports the existing client id instead of creating a duplicate [@ensure-personal-access-client]

Locale and admin

Command Purpose
users:align-fsp-locale {--dry-run} {--fsp=} Backfills user locale to match the FSP's default_locale override where one is configured, optionally scoped to one FSP by id or slug [@align-fsp-locale]
admin:create Creates a new admin user for the Filament admin panel [@admin-create]
admin:assign-role {--email=} {--role=} Assigns a shared RBAC access role (from AdminAccess::roleNames()) to an existing admin user, syncing permissions first [@admin-assign-role]
users:phone-dedup-report Reports users with duplicate non-null phone_number values [@phone-dedup-report]

Scheduled notification, alert, and maintenance jobs

The remaining commands are almost entirely driven by routes/console.php's Schedule::command(...) entries rather than run ad hoc. Grouped by what they maintain:

Command Schedule Purpose
notifications:task-reminders hourly Push reminder 24h after action assignment with no activation [@notifications-task-reminders] [@console-schedule]
notifications:task-window-closing hourly Push ~6h before an action expires
notifications:action-dropoff-reminders hourly Push for users who started but didn't complete an action
notifications:pre-alert-reengagement hourly Push to onboarded users with no engagement in 24h
notifications:season-kickoff manual Broadcast: seasonal preparedness kickoff push
notifications:feature-announcement manual Broadcast: announce a new feature via push
notifications:app-update manual, post-deploy Broadcast: nudge users to update the PWA [@notifications-app-update]
notifications:prune {--days=90} daily Deletes read manager notifications past retention
alerts:retarget hourly Post-alert retargeting SMS to users who haven't opened the app [@alerts-retarget]
alerts:retry-missed manual Re-dispatches messages to alert recipients never contacted due to batch failures [@alerts-retry-missed]
actions:expire hourly Expires assigned user actions past their expiration window
events:auto-close daily 00:30 Closes active events whose end_date has passed
marketing:process-flows every 15 min Processes active marketing flows and sends eligible touchpoints
messaging:poll-bonga-delivery every 5 min Polls BongaSMS delivery status for pending SMS messages
messaging:prune-delivery-logs daily Prunes and redacts old message delivery logs
slack:daily-digest daily 08:00 Sends a daily operations digest to Slack
exports:cleanup {--hours=1} hourly Deletes stale export files from the private exports directory

Development and one-off utilities

Command Purpose
app:convert-action-workbook {path} Converts an action workbook XLSX into seeder JSON (action_knowledge_base.json + action_translations.json) [@convert-action-workbook]
mixpanel:backfill-profiles {--dry-run} Backfills Mixpanel profile properties for existing users
push:test Sends a test FCM push and dumps the payload/response for debugging
test:retargeting-scenario {user_id} {--severity=red} Creates a test scenario for post-alert retargeting (an alert with a past event_date)