Skip to content

feat(enchantedparks): add Valleyfair under new Enchanted Parks umbrella#183

Open
cubehouse wants to merge 12 commits into
mainfrom
park/enchantedparks-valleyfair
Open

feat(enchantedparks): add Valleyfair under new Enchanted Parks umbrella#183
cubehouse wants to merge 12 commits into
mainfrom
park/enchantedparks-valleyfair

Conversation

@cubehouse
Copy link
Copy Markdown
Member

Summary

Restores Valleyfair coverage in parksapi after the cedarfair-drop refactor (8d7216ab, 2026-04-20) accidentally dropped it. That refactor assumed every former Cedar Fair park had moved to the unified Six Flags app; in fact Valleyfair was divested to a new operator, Enchanted Parks, and is no longer in the Six Flags Firebase Remote Config.

This PR adds an enchantedparks umbrella destination class with Valleyfair as the first concrete subclass. The umbrella shape is ready to host the four other Enchanted Parks (Worlds of Fun, Michigan's Adventure, Six Flags St Louis, Six Flags Great Escape, Schlitterbahn Galveston) in follow-up PRs.

What v1 covers

  • DESTINATION enchantedparks_valleyfair
  • PARK enchantedparks_park_VF (Valleyfair theme park)
  • PARK enchantedparks_park_VFW (Superior Shores Waterpark)
  • ATTRACTIONs for each park, scraped from /rides-and-experiences/<path>/
  • Schedules from the WordPress Tribe Events REST API (paginated; iCal feed fallback)
  • Live data: empty for v1

Live wait-times will plug in when the Enchanted Parks app launches Memorial Day 2026.

Architecture

  • Base class EnchantedParks (src/parks/enchantedparks/enchantedparks.ts) — shared HTTP fetches, pure scrape parsers (parseTribeEvents, parseICalFeed, parseAttractionsPage), getAttractionRideIds-style filter, and template-method build* implementations.
  • Concrete subclass Valleyfair (valleyfair.ts) — config-only (~40 lines).
  • Pure parsers are module-scope and exported, exercised by 19 unit tests using small inline fixtures.

Notable details

  • Tribe REST pagination. The plugin caps per_page at 50 regardless of what you ask for. scrapeSchedule walks pages via total_pages (30-page safety ceiling).
  • iCal fallback. If Tribe fails (404, 500, or zero matches in 90 days), parses the iCal feed.
  • Two-PARK attribution. The master /rides-and-experiences/attractions/ page lists every ride (theme park + waterpark together). The waterpark page (/rides-and-experiences/superior-shores-waterpark/) lists only its own rides — those slugs are claimed first, and theme-park-page entries with the same slug are skipped to avoid double-emit.

Test plan

  • npm run build clean
  • npm test — full 1107+ test suite passes
  • npm run dev -- valleyfair 4/4 passes:
    • 1 destination, 2 parks, ~52 attractions
    • 2 schedule blocks, ~168 total days
  • Unit tests for all three parsers (19 tests total)
  • Regression test (entityIdRegression.test.ts) updated to assert the new registry id and namespaced entity id

Migration

After merge, run npm run migrate -- valleyfair --wiki-slug=valleyfair to retag wiki entities from the old valleyfair/* IDs to the new enchantedparks_* IDs.

🤖 Generated with Claude Code

cubehouse and others added 10 commits May 11, 2026 22:43
Base class EnchantedParks extends Destination with @config subdomain/
destinationId/destinationName/timezone plus optional themePark/waterPark
ParkConfig blocks. getDestinations returns the DESTINATION entity;
buildEntityList / buildSchedules / buildLiveData are stubs that return
empty arrays. Valleyfair subclass provides the concrete config.

Restores Valleyfair coverage that was dropped in 8d7216a when the
unified sixflags class was assumed to cover all former Cedar Fair
parks; in fact Valleyfair was divested to Enchanted Parks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure module-scope function that filters The Events Calendar REST
response to events whose categories include the requested name (e.g.
'Park Hours' for the theme park, 'Waterpark Hours' for the waterpark)
and converts them to operating-hours schedule entries with the correct
timezone offset. Skips all_day events (those are group events, not
operating hours).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Used when the Tribe REST endpoint is unavailable. Parses VEVENT blocks,
filters by CATEGORIES line, skips all-day events (DTSTART;VALUE=DATE).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks <a href="…/rides-and-experiences/attractions/{slug}/"> links
and pairs each with the nearest h3 heading. Decodes HTML entities,
deduplicates repeats, ignores dining/etc links.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cache 1h for the calendar sources (events refresh hourly on the WP
side) and 24h for the static rides listing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…back

The Tribe REST endpoint caps each page at 50 events. scrapeSchedule
pages through until total_pages is reached (with a 30-page safety
ceiling), then parses each page's category-filtered events. Falls
back to the iCal feed only if Tribe returned zero matches or threw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Theme-park rides from themePark.ridesPath, waterpark rides from
waterPark.ridesPath. The waterpark slugs are claimed first; the
master attractions page (which lists everything together) skips
those so each ride emits exactly once under the right park.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Theme park reads 'Park Hours' category, waterpark reads 'Waterpark
Hours' category. Tribe REST is preferred (paginated); iCal feed is
used as fallback if Tribe is unavailable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 15, 2026 19:22
h3 = candidate;
}
if (!h3) continue;
const name = decodeHtmlEntities(h3[1].replace(/<[^>]+>/g, '').trim());
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new enchantedparks destination implementation to restore Valleyfair coverage after the Cedar Fair refactor dropped it, establishing an “Enchanted Parks” umbrella intended to host additional parks in follow-up PRs.

Changes:

  • Introduces EnchantedParks base class with schedule + attraction scraping (Tribe Events REST + iCal fallback, and HTML parsing).
  • Adds Valleyfair concrete destination under the new umbrella with two PARK entities (theme park + waterpark).
  • Adds unit tests for the pure parsers and updates entity/registry regression coverage for the new Valleyfair registration.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/parks/enchantedparks/valleyfair.ts Registers Valleyfair destination and provides default IDs/metadata for destination + two parks
src/parks/enchantedparks/enchantedparks.ts Implements shared Enchanted Parks scraping + entity/schedule builders and exported parsers
src/parks/enchantedparks/__tests__/enchantedparks.test.ts Adds unit tests for parseTribeEvents, parseICalFeed, parseAttractionsPage
src/__tests__/entityIdRegression.test.ts Adds regression assertions for Valleyfair registry id/category and DESTINATION entity id

Comment on lines +8 to +15
super({
...options,
config: {
subdomain: 'https://valleyfair.enchantedparks.com',
destinationId: 'enchantedparks_valleyfair',
destinationName: 'Valleyfair',
timezone: 'America/Chicago',
...(options?.config ?? {}),
location?: {latitude: number; longitude: number};
};

@config
cubehouse and others added 2 commits May 15, 2026 19:31
Spec entity-id schema is enchantedparks_attraction_<CODE>_<slug>; the
initial implementation dropped the <CODE> segment, which would risk
cross-park slug collisions when other Enchanted Parks subclasses land
later. Adds 'code' to ParkConfig (e.g. 'VF', 'VFW'), threads it into
the id construction, and tightens the regression test to assert the
legacy 'valleyfair' id is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses two Copilot review notes on #183:

1. Subdomain was hardcoded in the Valleyfair subclass. CLAUDE.md
   requires empty defaults configured via .env. The base class's
   @config subdomain now resolves entirely from VALLEYFAIR_SUBDOMAIN
   (or ENCHANTEDPARKS_SUBDOMAIN). _init() throws a clear error if
   neither is set so failure is loud.

2. Class-level @config on the base risks double-wrap with the
   subclass's @destinationController. Removed the class decorator;
   property-level @config still resolves through the subclass proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants