iabduul7/laravel-themepark-booking-adapters

Self-contained, drop-in Laravel booking adapters for Walt Disney World & SeaWorld/United Parks (Redeam) and Universal Orlando (SmartOrder): catalog, pricing, availability, holds/bookings, orders and voucher data.

Maintainers

Package info

github.com/iabduul7/laravel-themepark-booking-adapters

pkg:composer/iabduul7/laravel-themepark-booking-adapters

Fund package maintenance!

iabduul7

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v4.0.0 2026-06-28 14:26 UTC

This package is auto-updated.

Last update: 2026-06-28 14:59:49 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub PHPStan Action Status Total Downloads

A Laravel package providing self-contained, drop-in booking adapters for major theme park distribution APIs:

Park Provider Adapter
Walt Disney World Redeam DisneyRedeamAdapter
SeaWorld / United Parks Redeam SeaWorldRedeamAdapter
Universal Orlando SmartOrder ("SmartOrder2") UniversalSmartOrder2Adapter

Each adapter exposes the provider's real API surface (catalog, rates, availability, pricing schedules, holds/bookings for Redeam; events/orders for SmartOrder) and returns lightweight DTOs over the raw responses. Auth, retries and OAuth token management are handled for you.

What's working today: all three adapters are contract-tested with Http::fake and analysed with PHPStan (level 3). Read paths (catalog, rates, pricing, availability) are additionally verified against the live provider sandboxes, and the full write lifecycle is proven live for Disney (Redeam hold → book → cancel) and Universal (SmartOrder place → cancel). SeaWorld / United Parks is contract-tested only — its sandbox (Discovery Cove) exposes no bookable availability.

Features

  • 🎢 Three production parks supported: Disney (Redeam), SeaWorld/United Parks (Redeam), Universal (SmartOrder)
  • 🧩 Driver-based resolution via a Laravel Manager + ThemePark facade
  • 🔐 Auth handled: Redeam X-API-Key/X-API-Secret; SmartOrder OAuth2 client-credentials with token caching and 401 self-heal
  • ♻️ Resilient transport: idempotent reads retried on connection drops / 5xx; writes never retried (no double-booking)
  • 📦 Typed result objects: Supplier, Product, Rate, PriceSchedule, RatePriceSchedule, Availability, Hold, Booking, …
  • 🎫 Voucher data exposed: tickets() normalises each booking/order response into typed TicketArtifacts (redeemable identifier + format + validity); barcode/PDF rendering stays in your app
  • 🧩 Capability interfaces: SupportsHolds, SupportsEvents, ProvidesTicketArtifacts — type-hint a capability instead of a concrete park
  • 🧱 No app coupling: pure API integration; persistence, jobs, commission/margins and voucher rendering stay in your app
  • 🧪 Contract-tested with Http::fake(), analysed with PHPStan, and exercised live against the provider sandboxes

Requirements

  • PHP 8.2, 8.3 or 8.4
  • Laravel 12.x or 13.x (illuminate/contracts ^12.0|^13.0)

Earlier releases targeted PHP 8.1 / Laravel 10–11. Support for those was dropped in 3.0.0 to stay on the security-patched Laravel line (≥ 12.60 / ≥ 13.10); see the changelog.

Installation

composer require iabduul7/laravel-themepark-booking-adapters

Publish the config file:

php artisan vendor:publish --tag="themepark-adapters-config"

Add credentials to your .env (see .env.example for the full list):

THEMEPARK_DEFAULT_PROVIDER=disney

# Disney (Redeam)
REDEAM_DISNEY_SUPPLIER_ID=your_disney_supplier_id
REDEAM_DISNEY_API_KEY=your_disney_api_key
REDEAM_DISNEY_API_SECRET=your_disney_api_secret

# SeaWorld / United Parks (Redeam) — supplier is passed per call
REDEAM_UNITED_PARKS_API_KEY=your_seaworld_api_key
REDEAM_UNITED_PARKS_API_SECRET=your_seaworld_api_secret

# Universal (SmartOrder)
SMARTORDER_CUSTOMER_ID=134853
SMARTORDER_APPROVED_SUFFIX=-2KNOW
SMARTORDER_CLIENT_USERNAME=your_client_username
SMARTORDER_CLIENT_SECRET=your_client_secret

Usage

Resolving an adapter

use Iabduul7\ThemeParkAdapters\Facades\ThemePark;

$disney    = ThemePark::provider('disney');     // DisneyRedeamAdapter
$seaworld  = ThemePark::provider('seaworld');   // SeaWorldRedeamAdapter
$universal = ThemePark::provider('universal');  // UniversalSmartOrder2Adapter

You can also resolve via the container (app('themepark') / ThemeParkManager) or construct an adapter directly with a config array:

use Iabduul7\ThemeParkAdapters\Providers\Disney\DisneyRedeamAdapter;

$disney = new DisneyRedeamAdapter(config('themepark-adapters.providers.disney'));

Disney (Redeam) — supplier fixed from config

$products = $disney->getAllProducts();                                  // Product[]
$product  = $disney->getProduct('PRODUCT_ID');                          // Product
$rates    = $disney->getProductRates('PRODUCT_ID');                     // Rate[]
$schedule = $disney->getProductPricingSchedule('PRODUCT_ID', '2026-06-01', '2026-06-30'); // PriceSchedule
$rateSched = $disney->getProductRatePricingSchedule('PRODUCT_ID', '2026-06-01', '2026-06-30', 'RATE_ID');
$avail    = $disney->checkAvailabilities('PRODUCT_ID', '2026-06-01', '2026-06-30');

// Hold → book → cancel
$hold    = $disney->createNewHold(['hold' => ['items' => [/* … */]]]);
$booking = $disney->createNewBooking(['booking' => [/* … */]]);
$disney->deleteBooking('BOOKING_ID');

echo $product->getName();
echo $product->getId();

SeaWorld / United Parks (Redeam) — supplier passed per call

$products = $seaworld->getAllProducts('SUPPLIER_ID');
$product  = $seaworld->getProduct('SUPPLIER_ID', 'PRODUCT_ID');
$rates    = $seaworld->getProductRates('SUPPLIER_ID', 'PRODUCT_ID');
$schedule = $seaworld->getProductPricingSchedule('SUPPLIER_ID', 'PRODUCT_ID', '2026-06-01', '2026-06-30');

Universal (SmartOrder)

$catalog = $universal->getAllProducts();                 // GET smartorder/MyProductCatalog
$months  = $universal->getAvailableMonths();             // next 12 months
$events  = $universal->findEvents([/* plu, dates, … */]); // POST smartorder/FindEvents
$order   = $universal->placeOrder([/* order lines, … */]);// POST smartorder/PlaceOrder

if ($universal->canCancelOrder(['ExternalOrderId' => 'E1'])) {
    $universal->cancelOrder(['ExternalOrderId' => 'E1']);
}

The adapters mirror the method names and signatures of the production LaravelRedeamForWaltDisney, LaravelRedeamForUnitedParks and SmartOrderClient clients so they can serve as a drop-in replacement. A normalised, provider-agnostic interface is proposed for a future major version.

Capability interfaces

  • Contracts\Capabilities\SupportsHoldscreateNewHold, getHold, deleteHold, createNewBooking, getBooking, deleteBooking (Redeam adapters)
  • Contracts\Capabilities\SupportsEventsfindEvents, placeOrder, getExistingOrder, canCancelOrder, cancelOrder (SmartOrder adapter)

Type-hint these when you only need a capability rather than a specific park.

Optional building blocks

Business logic that is specific to a deployment is kept out of the core adapters and shipped as opt-in helpers under Support/:

  • Support\Redeam\OptionCodeResolver — maps a Walt Disney World ticket name to its Redeam option code (also exposed on the Redeam adapters as getOptionCode() for drop-in parity).

Persistence (Eloquent models, migrations), queue/sync jobs, commission/operator margins and voucher rendering (barcode images, templates, PDF, delivery) are intentionally left to the consuming application. The package does expose the provider-native voucher datatickets() normalises each booking/order response into typed TicketArtifacts (the redeemable identifier + format + validity).

Authentication

  • RedeamX-API-Key / X-API-Secret headers; GET is form-encoded, writes are JSON.
  • SmartOrder — OAuth2 client-credentials (/connect/token, scope=SmartOrder); bearer token cached via a TokenRepository (set SMARTORDER_TOKEN_CACHE=false to always refresh, matching upstream); customerId injected into every request; transparent refresh-and-retry on 401.

Error handling

Any non-2xx provider response is raised as a ThemeParkApiException — reads and writes alike (a failure is never silently returned as an empty array). The exception carries the HTTP status via getCode() and the decoded provider error body via getResponseData(). Single-entity lookups raise a dedicated 404: getProduct() throws productNotFound(), getExistingOrder() throws orderNotFound().

use Iabduul7\ThemeParkAdapters\Exceptions\ThemeParkApiException;

try {
    $product = ThemePark::provider('disney')->getProduct('PRODUCT_ID');
} catch (ThemeParkApiException $e) {
    $status = $e->getCode();          // e.g. 404, 422, 503
    $body   = $e->getResponseData();  // decoded provider error payload (nullable)
    report($e);
}

Idempotent reads are retried on connection drops / 5xx before the failure surfaces; writes are never retried (no double-booking).

Development

composer install

Testing

# Adapter contract tests (Http::fake)
composer test:adapters

# Static analysis
composer analyse

# Code style
composer format        # fix
composer format:check  # check only

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Credits

License

The MIT License (MIT). Please see License File for more information.