Skip to content

Legacy App & Togoparts (TGP) Integration

The original TogoActive production Laravel app at /var/www/togoactive (the "old admin") is still live and still runs the cron machinery that powers activity ingestion, leaderboards, donation ranking, and achievements for every event launched before v3. The new v3 stack (admin backend, admin frontend, WL API, WL web) does not replace it — the two systems run side by side against the same production database, with a deliberate ownership split (legacy owns events < 49 end-to-end; for events >= 49 v3 owns achievement notifications and donation recalc, while achievement awarding still runs on legacy crons). Both systems also depend on a second, external platform: Togoparts (TGP), whose production database (mysql_tgp) holds the raw Strava/activity data and the stored procedures that compute ranks. This document explains how the coexistence works, how TGP is wired in, which cron owns what, and the rules you must not break.

Related docs: ./01-architecture-overview.md · ./04-wl-api.md · ./06-database-schema.md


1. Why two systems share one database

  • The legacy app and the v3 apps (admin backend + WL API) all point at the same TGA production database (the mysql connection in the legacy app). There is no data migration or sync layer between "old" and "new" — they are two codebases over one schema.
  • Consequence for operations: anything stored in the database — configuration rows, event settings, feature flags, content — edited from the new admin takes effect for the legacy app immediately (and vice versa). Only code changes require a deploy, and deploys are per-server (the legacy app, the v3 admin backend, and the v3 WL API live on different servers/paths).
  • The legacy app additionally opens a cross-database connection to Togoparts production (mysql_tgp) for Strava/activity/challenge data and rank stored procedures (see §3). The v3 WL API uses the same TGP integration for its own donation-ranking cron.
  • Because the legacy scheduler hammers the shared DB with per-event crons, it protects itself with a DB-CPU self-throttle (isDbHigh, see §8) and deliberate minute-staggering of cron start times.

2. Ownership split: legacy vs v3

The < 49 / >= 49 rule

The dividing line is event ID 49 (SG61, the first v3-native event) — but note the split applies to the notification/email step, not to awarding. The legacy generic AchievementMasterCron (minutes 2,12..52) still awards achievements — inserting challenge_achievement_winners rows — for every event with an open window (registration started, results date not more than an hour past), v3 events included:

Event IDsAchievement awarding (winner rows)Achievement emails / notifications sent by
< 49Legacy AchievementMasterCron + bespoke per-event cronsLegacy ChallengeNotification cron
>= 49Legacy AchievementMasterCron (still)v3 achievements:notify cron

The handshake between the two systems is the shared achievement-winner table challenge_achievement_winners and its notified flag. The legacy cron inserts winner rows; whichever notification engine owns the event sends and flips the flag. Because both systems read and write the same table, exactly one notification engine must own each event or participants get duplicate (or zero) notifications.

The guard lives in the legacy app, app/Console/Commands/ChallengeNotification.php (lines ~165–184):

php
protected function achievementEmailsEnabled($eventId): bool
{
    // Events < 49 → this cron owns them. Events >= 49 → v3 wl-api achievements:notify cron.
    return (int) $eventId < 49;
}

The SG61 story

Event 49 (SG61, TGP cid 119) was the crossover event. The legacy app has a fully-built TogoSg61AchievementCron command — it is still registered in Kernel.php — but its scheduler entry in config/schedule.php (lines ~338–349) is commented out and disabled, with an explicit note:

DISABLED: event 49 (SG61) achievements are handled by the v3 system. Do NOT re-enable without confirming v3 is not also assigning them.

If it were enabled it would run at minutes 5,20,35,50 with an end date of 2026-09-15 18:00. Leave it disabled: event 49's winners are already awarded by the generic AchievementMasterCron (with the emails sent by v3's achievements:notify); re-enabling the bespoke cron would double-assign winners in the shared challenge_achievement_winners table.

3. The TGP (Togoparts) platform integration

TGP is a separate production platform with its own database. The legacy app (and v3 WL API) integrate with it through three channels:

3.1 Direct DB access (mysql_tgp connection)

Defined in config/database.php (DATABASE_URL_TGP / DB_HOST_TGP / etc., utf8mb4_unicode_520_ci, strict => false). This connection holds:

  • Strava activity logs (challenge_activities_log — the raw ingest queue)
  • Challenge users / challenge membership data
  • The rank stored procedures below

3.2 Stored procedures (called on mysql_tgp, results land in shared TGA tables)

Stored procedurePurpose
UpdateRanksRecompute distance/standard leaderboard ranks for a challenge
UpdateHoursRanksRecompute hours-based (time) leaderboard ranks
UpdateDonationRanksRecompute individual donation-fundraising ranks
UpdateTeamDonationRanksRecompute team donation-fundraising ranks
UpdateTeamRanksRecompute team activity leaderboard ranks
UpdateGroupDonationRanksRecompute group-level donation ranks
UpdateSeasonalRanksRecompute per-season ranks (seasonal events, e.g. TogoRide)
GetSuspiciousActivitiesFlag anomalous/suspicious activities for review

Critical parameter rule: the donation-rank procedures take an excluded user ids parameter. Passing NULL causes the procedure to rank nobody — you must pass an empty string '' when there are no exclusions. (The v3 donations:recalc cron was bitten by this; the rule applies to any caller.)

3.3 HTTP bridges

  • ChallengeNotification POSTs to {domain}/api.php on TGP to deliver achievement notifications.
  • DonationCaculationCronUrlHit HTTP-hits {domain}/api/calculate/team and {domain}/api/calculate/individuals per active event to trigger donation recalculation. These calculate URLs 404 on v3 events — v3 replaced this pattern (see §5).

3.4 Event → challenge id mapping

A TGA event maps to a TGP challenge via a static map covering events 8–37, with a fallback to events_meta.TGP_CHALLENGE_ID for newer events. Admins set the CID field in the new admin, which writes that meta row. (Example: event 46 → cid 116, event 49/SG61 → cid 119.) UpdateDonationRank only processes events >= 33 that have a TGP_CHALLENGE_ID.

4. Activity pipeline (Strava → leaderboards → ranks)

The end-to-end flow, all orchestrated by legacy crons:

Strava API
   │  (Stage 2: ingest)

challenge_activities_log (TGP DB, unsynced rows)  ──►  activities (TGA DB)
   │  (Stage 3: per-event leaderboard sync)

TGA leaderboard tables  ──►  CALL rank stored procedures on mysql_tgp
  1. Stage 2 ingest — LeaderboardStage2Sync (every 3 minutes, global): reads unsynced challenge_activities_log rows on TGP, fetches full activity data from the Strava API, and writes into the activities tables. Supports --mode=live|backlog. A one-off strava:drain-backlog command drains historical backlog and pre-marks users with revoked Strava tokens.
  2. Dedup & fraud sweeps (global, every 2 hours): DuplicateActivity (minute 0), DuplicateActivityCronUrlHit (minute 20), and SuspiciousActivity (minute 40, CALL GetSuspiciousActivities).
  3. Stage 3 leaderboard sync (per event, staggered): the LeaderBoardSync*Stage3 family writes TGA leaderboard tables and then calls the TGP stored procedures to recompute ranks:
    • LeaderBoardSyncStage3OutdoorActivities — main per-event sync; --id --type=outdoor|indoor --mode=activity; calls UpdateDonationRanks / UpdateHoursRanks / UpdateRanks.
    • LeaderBoardSyncOutdoorActivitiesStage3 — event-43 sibling variant.
    • SeasonalLeaderBoardSyncOutdoorActivitiesStage3--season; calls UpdateSeasonalRanks.
    • LeaderBoardSyncTeamActivitiesStage3 — team sync; calls UpdateDonationRanks + UpdateTeamDonationRanks + UpdateTeamRanks.
    • LeaderBoardSyncTeamActivitiesForCHStage3 — company variant (not scheduled).
    • LeaderBoardSyncGroupActivitiesStage3 — calls UpdateGroupDonationRanks.
    • LeaderBoardSyncActivitiesImagesStage3 — syncs activity images.
  4. Utility commands: clone:event-activities and resync:event for manual repair/backfill.

5. Donation ranking pipeline: legacy vs v3

Two parallel mechanisms exist; which one applies depends on the event's owner. (One caveat: the legacy UpdateDonationRank cron is not id-gated — it processes every event ≥ 33 with a TGP_CHALLENGE_ID, v3 events included. That overlap is harmless because rank recomputes are idempotent; it is the calculate-URL mechanism that only works for legacy events.)

Legacy (events < 49)

  • UpdateDonationRank (every 3 minutes): for events >= 33 that have a TGP_CHALLENGE_ID, calls CALL UpdateDonationRanks + CALL UpdateTeamDonationRanks on mysql_tgp, excluding the host's TGP user ids from ranking.
  • DonationCaculationCronUrlHit (minutes 10,25,40,55): HTTP-hits {domain}/api/calculate/team and /api/calculate/individuals per active event (events 25, 23, 33 excluded) to recompute raised amounts.
  • achievement:remove-invalid-donation-winners — one-off cleanup command, dry-run by default.

v3 (events >= 49)

  • The legacy /api/calculate/* URLs 404 for v3 events — do not point the URL-hitter at them.
  • The v3 WL API runs its own donations:recalc cron every 10 minutes: a sweep that recomputes donation totals and calls the same TGP rank stored procedures (remember: pass '', never NULL, for the excluded-ids parameter).
  • v3 also exposes token-checked internal recalc endpoints that replace the URL-hit pattern for event-driven recalculation:
    • internal/events/{id}/recalc-targets
    • internal/events/{id}/recalc-individuals
    • internal/events/{id}/teams/{teamId}/recalc-target

6. Achievement pipeline

Per-event achievement crons (legacy)

Historically each event got a bespoke achievement cron in app/Console/Commands; newer events use the generic master cron.

CommandEvent
AchievementMasterCron (--id)Generic, any event; uses AchievementUnlockService
Decypher202636
RacetoRaise32
SecMoveEarthSEC Move the Earth
Cyclehome2025Cyclehome 2025
Gomad2024 / Gomad2025GoMad
CwaAchievementCronCWA
LBCyclethoneAchievementCronLB Cyclethone
TogoRide2026AchievementCron43
TogoSg60SG60
TogoSg6149 — registered but NOT scheduled; awarding runs via AchievementMasterCron, notification via v3 (§2)
togoride2024 chunk/season commandsTogoRide 2024
TogoRide2025/ subdir: YearLongAchievementCron, Season1AchievementCronSeason4AchievementCron, seasonal-badge (daily 18:01)TogoRide 2025
PortUserCron / port:userUser porting utility

A trash/ subdirectory holds retired crons.

Notification flow

  • ChallengeNotification (--id --userid): reads unnotified rows from challenge_achievement_winners, POSTs the notification to {domain}/api.php on TGP, marks notified. Contains the < 49 / >= 49 ownership guard (§2). Scheduled per event (staggered) plus a global no-id run at minutes 5,20,35,50.
  • notification-management-command — every minute, general notification dispatch.
  • MailForCompleteRewardPayment — abandoned-checkout reminder emails.
  • app:achievment-reward-unlock — reward unlock processing (note the "achievment" spelling in the signature).
  • hiturl — clears/warms the achievement page cache and health-checks it (minutes 7,17..57).

7. Full cron reference

Legacy scheduler behavior (app/Console/Kernel.php): schedule() runs only when APP_ENV == production. It is data-driven — it loops over config('schedule.commands'), honoring per-entry startDateTime/endDateTime gates, cronExpression or frequency, before/after logging hooks (writing to the cronJobs log channel), and skipWhen (isDbHigh, §8). A few jobs are hard-coded in the Kernel outside the config array (noted below).

Global (not event-specific)

CommandCadenceNotes
LeaderboardStage2Sync*/3 * * * *Core Strava ingest (Stage 2)
DuplicateActivity0 */2 * * *Dedup sweep
DuplicateActivityCronUrlHit20 */2 * * *Dedup via URL hit
SuspiciousActivity40 */2 * * *CALL GetSuspiciousActivities
UpdateDonationRank*/3 * * * *All events ≥ 33 with TGP_CHALLENGE_ID
DonationCaculationCronUrlHit10,25,40,55 * * * *Legacy calculate-URL hitter (404s on v3 events)
ChallengeNotification (no id)5,20,35,50 * * * *Global notification pass
AchievementMasterCron (no id)2,12,22,32,42,52 * * * *Generic achievement awarding — sweeps all events with an open window (registration started, results date ≤ 1 h past), incl. events ≥ 49
notification-management-command* * * * *
TriggerManagementCron --activity-sync* * * * *Full run: 0 */6 * * *
app:random-assign-profile-picture-cron* * * * *
app:db-cpu-report-command* * * * *Feeds isDbHigh; no skipWhen (must always run)
hiturl7,17..57 * * * *Achievement page cache/health
generate:csv / generate-csvevery minute (hard-coded)Loops over CsvJob rows
cleanup:old-configurations0 1,13 * * *
logs:clean0 2 * * *
TR25 seasonal-badgedaily 18:01 (hard-coded)
TR25 YearLongAchievementCronhourly at :30 (hard-coded, 2025 window)
TR26 (43) seasonal blockhard-coded: season achievement cron hourly at :15; SeasonalLeaderBoardSync outdoor+indoor hourly, dropping to every 6 h during a 10-day post-season buffer

Per-event (minute-staggered by design — see §8)

EventCommandMinutesEnd date
37 AMP UP!Outdoor Stage 3*/32026-02-20
37Indoor Stage 31,4..582026-02-20
37ChallengeNotification2,12..522026-02-20
37Team Stage 34,14..542026-02-20
37Images Stage 36,16..562026-02-20
36 DecypherOutdoor Stage 3*/32026-04-03 18:00
36Notify3,13..532026-04-03 18:00
36Team Stage 35,15..552026-04-03 18:00
36Decypher2026 achievements7,17..572026-04-03 18:00
36 (+45)Images Stage 39,19..59
32 RaceToRaiseOutdoor Stage 3*/32026-02-28
32Indoor Stage 32,5..592026-02-28
32RacetoRaise achievements1,11..512026-02-28
32Notify8,18..582026-02-28
32Team Stage 3*/102026-02-28
SEC Move EarthSecMoveEarth achievements3,13..532026-05-25
43 TogoRide 2026Outdoor Stage 3*/102027-01-23 18:00
43Indoor Stage 35,15..552027-01-23 18:00
43Notify4,14..542027-01-23 18:00
43Team Stage 36,21,36,512027-01-23 18:00
43TogoRide2026AchievementCron8,23,38,532027-01-23 18:00
43Images Stage 312,27,42,572027-01-23 18:00
49 SG61TogoSg61AchievementCron5,20,35,50 DISABLED(would be 2026-09-15 18:00)
33Outdoor Stage 3*/32026-12-31
33Indoor Stage 32,7..572026-12-31
33Team Stage 313,23..532026-12-31
33Images Stage 312,27,42,572026-12-31
45Outdoor Stage 3*/52026-05-18
45Indoor Stage 33,8..582026-05-18
45Team Stage 311,21..512026-05-18
45Notify6,16..562026-05-18
46Outdoor Stage 32,7..572026-12-31
46Team Stage 317,27..572026-12-31
46Notify1,11..512026-12-31
46Images Stage 314,24..542026-12-31
46Group Stage 30,10..502026-12-31
47 #STRIKEFORCARE2026Outdoor Stage 34,9..59
47Team Stage 319,29..59
47Notify3,13..53
47Images Stage 316,26..56
48Outdoor Stage 3*/5
48Indoor Stage 33,8..58
48Team Stage 311,21..51
48Notify7,17..57
48Images Stage 318,28..58

(Notation a,b..z = every 10 minutes starting at minute a; e.g. 2,12..52 = minutes 2,12,22,32,42,52.)

Unscheduled / utility commands

ManageTgpEventsListCron, LeaderBoardSyncTeamActivitiesForCHStage3, strava:drain-backlog, achievement:remove-invalid-donation-winners, clone:event-activities, resync:event, PortUserCron/port:user, plus make-service / sushi-model scaffolders. Retired crons live in app/Console/Commands/trash/.

8. DB-load protection

Two mechanisms keep the shared DB alive under this cron volume:

  1. isDbHigh self-throttle. Nearly every scheduled entry carries 'skipWhen' => 'isDbHigh' (implemented in app/Helpers/general.php:846). The signal is produced by app:db-cpu-report-command, which runs every minute (deliberately without a skipWhen) and reads thresholds/connection details from config/db_cpu_conf.php. When DB CPU is high, scheduled runs are skipped rather than queued.
  2. Minute staggering. Every per-event cron in config/schedule.php starts at a different minute offset (documented in the config comments) so that outdoor/indoor/team/images/notify jobs for different events never fire simultaneously. When adding a scheduled command, pick an unused minute offset — do not add another * * * * * or a */3 aligned with existing ones.

9. Legacy config inventory

From /var/www/togoactive/config:

Database connections (database.php)

ConnectionTargetNotes
mysql (default)Shared TGA production app DBDB_HOST etc.; utf8mb4; strict => false
mysql_tgpTGP production DBDATABASE_URL_TGP/DB_HOST_TGP...; utf8mb4_unicode_520_ci; strict => false. Holds Strava logs, challenge users, rank stored procedures
mysql_tga_stageTGA staginglatin1
mysql_wpWordPress DB
Redispredis, default + cache

Other config

  • db_cpu_conf.php — DB-CPU monitor config. ⚠️ Security cleanup item: contains a hardcoded fallback to the TGP production DB host (tgp-production-db-do-user-3163646-0.b.db.ondigitalocean.com, port 9273) with a plaintext credential fallback (user prom2qjr, password committed in the file). This should be moved to environment variables and the credential rotated.
  • filesystems.phplocal / public / s3 / do; the do disk is DigitalOcean Spaces (DO_BUCKET, DO_CDN_ENDPOINT, …) serving images/assets.
  • mail.php — default smtp mailer pointed at a Mailgun host, plus a failover mailer. services.php carries Mailgun/Postmark/SES credentials blocks.
  • onesignal.php — OneSignal push notification config.
  • queue.php — default queue is sync: jobs run inline in the request/cron process; there are no queue workers to deploy or restart for the legacy app.
  • No Stripe keys in config. The TGP integration surface is exactly: the mysql_tgp DB connection, the stored procedures, and the HTTP POSTs to {domain}/api.php and {domain}/api/calculate/*.
  • schedule.php — the data-driven cron table described in §7.
  • Miscellaneous app config: event-setting.php, event-meta.php, achievement-setup-form.php, leaderboard.php, leaderboard-default-style.php, miscellaneous.php, menu-setting.php, permission.php, avatars.php, image.php, larabug.php, pulse.php, datatables.php.

10. What developers must NOT do

  1. Do not re-enable TogoSg61AchievementCron in config/schedule.php. Event 49's winners are already awarded by the generic AchievementMasterCron (with emails from v3's achievements:notify); re-enabling the bespoke cron would double-assign winners in the shared challenge_achievement_winners table. The config comment says the same.
  2. Do not widen achievementEmailsEnabled() in ChallengeNotification.php past < 49 without first disabling the corresponding v3 achievements:notify ownership — the split is the entire coexistence contract.
  3. Do not point DonationCaculationCronUrlHit (or anything else) at /api/calculate/* for v3 events — those URLs 404 on v3. Use the v3 donations:recalc cron and the token-checked internal/events/{id}/recalc-* endpoints instead.
  4. Never pass NULL as the excluded-user-ids parameter to the TGP donation-rank stored procedures (UpdateDonationRanks, UpdateTeamDonationRanks, …). NULL makes the procedure rank nobody; pass '' when there are no exclusions.
  5. Do not add a scheduled command without a skipWhen => isDbHigh (unless, like app:db-cpu-report-command, it is the monitor) and without picking a fresh minute stagger. The shared DB is the bottleneck for both legacy and v3.
  6. Do not assume a config/DB change needs a legacy deploy — the DB is shared, so data changes are live everywhere instantly. Conversely, do not assume a code change is live everywhere — code is deployed per server.
  7. Do not print or copy the db_cpu_conf.php fallback credentials anywhere; treat them as compromised-by-commit and schedule rotation.
  8. Remember the legacy scheduler only runs under APP_ENV=production — crons silently doing nothing on a staging copy is expected behavior, not a bug.

Organiser guide and developer documentation for the TogoActive platform.