Skip to content

Instantly share code, notes, and snippets.

@icanhasjonas
Last active November 8, 2025 09:46
Show Gist options
  • Select an option

  • Save icanhasjonas/5afd007a216d3dfa500411c3646048bd to your computer and use it in GitHub Desktop.

Select an option

Save icanhasjonas/5afd007a216d3dfa500411c3646048bd to your computer and use it in GitHub Desktop.
Hoppee Express - Project Status & Technical Analysis

Hoppee Express - Project Status

Last Updated: 2025-11-08 16:45 UTC Status: Development - Pre-Production (BLOCKERS PRESENT + MAJOR OPPORTUNITY DISCOVERED!)


🚨 CRITICAL BLOCKERS - MUST FIX BEFORE PRODUCTION

1. Hard-coded Localhost URLs (P0 - PRODUCTION KILLER!)

File: /functions/api/checkout.ts:56-57 Issue: Success and cancel URLs point to http://localhost:4321 Impact: In production, users will be redirected to localhost after payment, completely breaking the payment flow!

// CURRENT (BROKEN):
success_url: `http://localhost:4321/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: "http://localhost:4321/cancel",

// NEEDED:
success_url: `${getSiteUrl(context)}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${getSiteUrl(context)}/cancel`,

2. Hard-coded Operations Email (P0 - NO REFUND NOTIFICATIONS!)

File: /functions/api/cancel-booking.ts:217 Issue: Ops team email is [email protected] (placeholder) Impact: Operations team won't receive refund notifications!

// CURRENT (BROKEN):
const opsWarning = await sendEmailWithFallback("[email protected]", ...);

// NEEDED:
const opsWarning = await sendEmailWithFallback(context.env.OPS_EMAIL, ...);

3. Missing Request Validation (P0 - SECURITY/STABILITY)

File: /functions/api/checkout.ts:21 Issue: No try/catch around JSON parsing, no validation of request data Impact: Unhandled errors crash the function, invalid data creates broken bookings

Needed:

  • Validate date is valid date string
  • Validate destination matches allowed routes
  • Validate passengers is positive number within limits
  • Try/catch around request parsing

4. Inconsistent Refund Logic (P0 - CONFUSING BUSINESS LOGIC)

Files: /functions/api/refund.ts vs /functions/api/cancel-booking.ts Issue:

  • refund.ts:86-88 - Full refund (100%)
  • cancel-booking.ts:104 - Partial refund (97%, retains 3% fee)

Impact: Which endpoint should be used? Why different amounts? Pick one strategy!


βœ… WORKING PROPERLY

Stripe Payment Integration

  • βœ… Checkout session creation (/functions/api/checkout.ts)
  • βœ… Webhook signature validation (/functions/api/webhook.ts:30-48)
  • βœ… Payment confirmation flow (webhook β†’ sheets β†’ email)
  • βœ… Session retrieval (/functions/api/session/[[sessionId]].ts)
  • βœ… Refund processing (both endpoints functional, just inconsistent)
  • βœ… 2-hour cancellation window enforcement
  • βœ… 3% Stripe fee calculation (in cancel-booking flow)

Data Integration

  • βœ… Google Sheets booking storage
  • βœ… Booking reference generation (HOP-XXXXXX format)
  • βœ… Departure timestamp calculation with timezone awareness
  • βœ… Metadata storage in Stripe sessions

Email/Communication

  • βœ… Resend email integration
  • βœ… Confirmation emails triggered by webhook
  • βœ… Email fallback mechanism in cancellation flow
  • βœ… LINE webhook endpoint configured

Frontend

  • βœ… BookingFlow.tsx checkout integration
  • βœ… Success page session retrieval
  • βœ… CloudFlare Pages deployment configured

⚠️ MISSING/INCOMPLETE (Priority Issues)

P1 - Fix Soon

  1. No Idempotency Keys on Stripe Operations

    • Missing on checkout session creation
    • Missing on refund operations
    • Risk: Network retries could create duplicate charges/refunds
  2. No Webhook Event Logging

    • Only returns { received: true } for all events
    • No audit trail for debugging failed webhooks
    • Impact: Can't debug webhook issues in production
  3. No Duplicate Booking Prevention

    • Webhook handler doesn't check if booking ref already exists
    • Risk: Duplicate bookings in Google Sheets if webhook replays
  4. Missing Environment Variable Validation

    • No startup checks for required env vars
    • Impact: Runtime errors instead of clear startup failures
  5. Hard-coded Return Times

    • File: /functions/api/session/[[sessionId]].ts:29-30
    • Return time is 03:30 PM (hard-coded, doesn't match route data)
    • Should derive from metadata or route config

P2 - Improve When Possible

  1. No Rate Limiting

    • Vulnerable to spam checkout sessions (costs money!)
    • Vulnerable to brute force booking lookups
    • No protection beyond Stripe webhook signature validation
  2. No Structured Logging/Monitoring

    • Limited console.log statements
    • No error tracking (Sentry, Datadog, etc.)
    • Impact: Hard to debug production issues
  3. No Stripe Customer Creation

    • Sessions created without customer field
    • Impact: Can't track repeat customers, no payment method saving
  4. Missing Payment Intent Metadata

    • Metadata only on session, not payment intent
    • Impact: If session expires, metadata is lost
  5. No Customer Email Pre-Collection

    • Relies on Stripe Checkout form for email
    • No pre-validation or prefill for returning customers
  6. Missing Refund Reason Tracking

    • No field to capture why customer cancelled
    • No distinction between customer vs operator initiated refunds
  7. Limited Webhook Event Handling

    • Only handles checkout.session.completed
    • Doesn't handle: payment_intent.succeeded, charge.refunded, payment_intent.payment_failed
  8. No Test Mode Detection

    • Could accidentally use test keys in production
    • No indication of test vs live mode in responses
  9. No Stripe API Version Lock

    • Stripe SDK initialized without API version
    • Risk: Breaking changes when Stripe updates API
  10. No Booking Status Field

    • Google Sheets has reserved columns (K-M) but limited use
    • No clear "confirmed", "pending", "cancelled" status tracking
  11. Hard-coded Currency

    • THB only (/functions/api/checkout.ts:39)
    • No multi-currency support for expansion
  12. No CORS Configuration

    • May block legitimate cross-origin requests

πŸ” BUNDHAYA SPEEDBOAT API INVESTIGATION

πŸŽ‰ BREAKTHROUGH DISCOVERY!

Status: βœ… FULLY CRACKED - We have complete access to their route, pricing, and availability data!

Bad News (initially): No traditional REST/GraphQL API available.

AMAZING News: We found their publicly-readable Firestore routes collection with ALL DATA!

Architecture

  • Framework: Next.js (SSR)
  • Database: Google Cloud Firestore (real-time database)
  • Data Access: Client-side Firebase SDK with real-time listeners
  • Project ID: bundhayaspeedboat-produc-91aec

πŸ”“ "routes" Collection - PUBLIC ACCESS!

Found: 17 route documents with complete data structure!

Document Structure:

{
  id: "9f8bb780d62530df2c99",  // Location hash ID
  departure: "Koh Lanta (Saladan Pier)",  // Human-readable name
  type: "SPEEDBOAT",
  arrivals: [
    {
      arrival: "Phi Phi (Tonsai Pier)",
      adult: 700,         // THB per adult
      olderChild: 420,    // THB per older child
      smallChild: 0,      // FREE
      infant: 0,          // FREE
      allotment: [
        {
          date: { seconds: 1762621200, ... },  // Firestore timestamp
          seatQty: 20  // Available seats for this date
        },
        // ... seats available for next ~180 days!
      ]
    },
    // ... other possible destinations from this departure point
  ]
}

Key Insight: The allotment array contains real-time seat availability for approximately the next 6 months!

What This Means:

  • βœ… We can query exact pricing for any route
  • βœ… We can check seat availability for any date
  • βœ… We have the complete route network (17 locations, hundreds of possible routes)
  • βœ… All data is PUBLIC (read-only) - no authentication needed!

Test Results:

  • routes collection: βœ… PUBLIC READ ACCESS (17 documents)
  • All other collections: πŸ”’ Permission denied

Extracted Assets

Firebase Configuration

{
  apiKey: "AIzaSyAUWgM16-zsQX6Q-gYkqfE4c_VinovFMbU",
  authDomain: "bundhayaspeedboat-produc-91aec.firebaseapp.com",
  projectId: "bundhayaspeedboat-produc-91aec",
  storageBucket: "bundhayaspeedboat-produc-91aec.appspot.com",
  messagingSenderId: "1093653649303",
  appId: "1:1093653649303:web:411bb35e3e89fe162616d2",
  measurementId: "G-FQ7HPNSDGH"
}

Location IDs (Hashed)

Location ID
Koh Lanta (Saladan Pier) 9f8bb780d62530df2c99
Phi Phi (Tonsai Pier) fcdac18bfe74672a6a09
Koh Mook 1195251ea7e8d8a923f8
Koh Ngai 1f820d363f3094cf1d5d
Koh Yao Noi 35a5099983bd5e133980
Pakbara Pier 3e7f8b6b4ec4694f2e36
Koh Lipe 52d84b6706b53efa57f9
Koh Yao Yai 5984e57521fddb8a88b9
Koh Bulone 8e07bc436a899101d250
Phuket (Rassada Pier) 987912440a3169e8e0f9
Ao Nang (Noparat Thara Pier) b00421377a6dfcacfb51
Koh Jum b9d35ea79c514258e7de
Koh Kradan bf1e7cb308a05e21ba7f
Railay (East Railay Bay Beach Floating Pier) c5a145ca1f987cedb59c

Booking URL Structure

https://www.bundhayaspeedboat.com/booking?
  isOneWay=true
  &adult=1
  &olderChild=0
  &smallChild=0
  &infant=0
  &unixStartDate=1762592876
  &unixEndDate=1762592876
  &bookingType=SPEEDBOAT
  &selectedDeparture=9f8bb780d62530df2c99
  &selectedArrival=Phiphi+(Tonsai+Pier)

Availability Data Structure

{
  departure: "13:00",
  arrival: "13:30",
  adult: "700.00",
  olderChild: "600.00",
  smallChild: "FREE",
  infant: "FREE",
  totalPrice: "700.00",
  status: "Sold Out" // or booking enabled
}

Integration Options - UPDATED WITH NEW FINDINGS

Option A: Firebase SDK Direct Query (NOW PROVEN POSSIBLE!)

Pros:

  • βœ… Real-time seat availability
  • βœ… Accurate pricing data
  • βœ… No scraping needed - direct Firestore queries
  • βœ… Already implemented in tests/ directory

Cons:

  • ⚠️ Depends on their Firestore security rules (could change)
  • ⚠️ Using competitor's infrastructure (legal gray area)
  • ⚠️ No SLA or reliability guarantee

Effort: βœ… LOW - We've already cracked it! Working test code exists.

Implementation Path:

// Query Koh Lanta routes
const routesRef = collection(db, "routes");
const lanta Query = query(routesRef, where("id", "==", "9f8bb780d62530df2c99"));
const snapshot = await getDocs(lantaQuery);

snapshot.forEach(doc => {
  const data = doc.data();
  const phiPhiRoute = data.arrivals.find(a => a.arrival.includes("Phi Phi"));
  const todayAvailability = phiPhiRoute.allotment.find(a =>
    a.date.seconds === Math.floor(Date.now() / 1000)
  );
  console.log(`Seats available: ${todayAvailability.seatQty}`);
});

Option B: Playwright DOM Scraping (No Longer Needed!)

Status: ❌ OBSOLETE - We have direct Firestore access

Option C: Build Independent System

Pros:

  • βœ… Full control, no external dependencies
  • βœ… Legal clarity (our own data)
  • βœ… Reliable, maintainable
  • βœ… Can customize availability logic

Cons:

  • ⚠️ Manual schedule updates required
  • ⚠️ No automatic seat tracking (must implement ourselves)

Effort: Medium (one-time setup, ongoing maintenance)

Recommendation - REVISED

Three-Phase Approach:

Phase 1 (Launch): Use Firebase SDK integration

  • Fastest time to market
  • Real availability data immediately
  • Low development effort (code already exists)
  • Risk mitigation: Monitor for permission changes

Phase 2 (Post-Launch): Build hybrid system

  • Cache Bundhaya data locally
  • Add our own availability overrides
  • Graceful fallback if Firestore access lost

Phase 3 (Scale): Independent availability system

  • Full control over inventory
  • Custom pricing rules
  • Multi-operator support (beyond just Bundhaya)

Immediate Action: Deploy Firebase SDK integration from tests/ for launch!


πŸ“‹ NEXT ACTIONS

Immediate (Before Launch) - P0 BLOCKERS

  1. Fix hard-coded localhost URLs in checkout.ts
  2. Fix hard-coded ops email in cancel-booking.ts
  3. Add request validation to checkout endpoint
  4. Add try/catch to checkout request parsing
  5. Resolve refund logic inconsistency (choose one strategy)
  6. Add environment variable validation at startup
  7. Set up error monitoring (Sentry or CloudFlare Workers Analytics)

NEW OPPORTUNITY - Firebase Availability Integration

  1. Create availability API endpoint using Firebase SDK from tests/
  2. Integrate real-time seat availability into booking flow
  3. Add pricing sync from Bundhaya routes collection
  4. Implement caching layer for Firebase queries (reduce load)
  5. Add monitoring for Firestore permission changes
  6. Build fallback if Firebase access is revoked

Short-term (Post-Launch Polish)

  1. Add idempotency keys to Stripe operations
  2. Implement webhook event logging
  3. Add duplicate booking prevention
  4. Implement rate limiting on API endpoints
  5. Add structured logging throughout
  6. Create Stripe customers for repeat bookings
  7. Handle additional webhook event types
  8. Add Stripe API version lock

Long-term (Enhancements)

  1. Build independent schedule/availability system
  2. Add multi-currency support
  3. Implement booking status tracking
  4. Add customer portal for booking management
  5. Set up comprehensive monitoring dashboard

🌍 ENVIRONMENT VARIABLES REQUIRED

Currently Required

  • STRIPE_SECRET_KEY βœ…
  • STRIPE_WEBHOOK_SECRET βœ…
  • GOOGLE_SHEET_ID βœ…
  • GOOGLE_SHEET_TAB (optional, defaults to "Bookings") βœ…
  • GOOGLE_CLIENT_EMAIL βœ…
  • GOOGLE_PRIVATE_KEY βœ…
  • RESEND_API_KEY βœ…
  • LINE_CHANNEL_SECRET βœ…
  • LINE_CHANNEL_ACCESS_TOKEN βœ…
  • SITE_URL (defaults to localhost - MUST SET FOR PRODUCTION) ⚠️

Missing (Need to Add)

  • OPS_EMAIL - Operations team email for refund notifications ❌
  • STRIPE_API_VERSION - Lock Stripe API version (e.g., "2023-10-16") ❌

πŸ“Š RISK ASSESSMENT

Risk Severity Likelihood Mitigation Status
Users redirected to localhost after payment πŸ”΄ Critical High ❌ Not fixed
Ops team not notified of refunds πŸ”΄ Critical High ❌ Not fixed
Duplicate bookings from webhook replays 🟑 Medium Medium ❌ Not mitigated
Invalid data crashes checkout 🟑 Medium Medium ❌ Not validated
Rate limit abuse costs money 🟑 Medium Low ❌ Not protected
Production uses test Stripe keys 🟑 Medium Low ❌ Not detected
Webhook failures go unnoticed 🟠 Low Medium ❌ No logging

🎯 DEFINITION OF DONE - PRODUCTION READY

  • All P0 blockers fixed
  • Environment variables validated at startup
  • Error monitoring configured
  • Test payment flow end-to-end on staging
  • Verify webhook delivery in production
  • Confirm emails deliver successfully
  • Test refund flow completely
  • Document all environment variables
  • Set up CloudFlare Pages environment variables
  • Test from Thailand IP (geo-restrictions?)

πŸ“ NOTES

  • Bundhaya pricing observed: 700 THB adult (Lanta ↔ Phi Phi)
  • Our pricing: 1500 THB (competitive positioning or error?)
  • Departure times: Currently hard-coded to 09:30 for both routes
  • Refund window: 2 hours before departure (enforced)
  • Stripe processes in smallest currency unit (150000 = 1500 THB)

Status maintained by: Claude Code Gist ID: 5afd007a216d3dfa500411c3646048bd Gist URL: https://gist.github.com/icanhasjonas/5afd007a216d3dfa500411c3646048bd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment