Last Updated: 2025-11-08 16:45 UTC Status: Development - Pre-Production (BLOCKERS PRESENT + MAJOR OPPORTUNITY DISCOVERED!)
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`,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, ...);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
dateis valid date string - Validate
destinationmatches allowed routes - Validate
passengersis positive number within limits - Try/catch around request parsing
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!
- β
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)
- β Google Sheets booking storage
- β
Booking reference generation (
HOP-XXXXXXformat) - β Departure timestamp calculation with timezone awareness
- β Metadata storage in Stripe sessions
- β Resend email integration
- β Confirmation emails triggered by webhook
- β Email fallback mechanism in cancellation flow
- β LINE webhook endpoint configured
- β BookingFlow.tsx checkout integration
- β Success page session retrieval
- β CloudFlare Pages deployment configured
-
No Idempotency Keys on Stripe Operations
- Missing on checkout session creation
- Missing on refund operations
- Risk: Network retries could create duplicate charges/refunds
-
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
- Only returns
-
No Duplicate Booking Prevention
- Webhook handler doesn't check if booking ref already exists
- Risk: Duplicate bookings in Google Sheets if webhook replays
-
Missing Environment Variable Validation
- No startup checks for required env vars
- Impact: Runtime errors instead of clear startup failures
-
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
- File:
-
No Rate Limiting
- Vulnerable to spam checkout sessions (costs money!)
- Vulnerable to brute force booking lookups
- No protection beyond Stripe webhook signature validation
-
No Structured Logging/Monitoring
- Limited console.log statements
- No error tracking (Sentry, Datadog, etc.)
- Impact: Hard to debug production issues
-
No Stripe Customer Creation
- Sessions created without
customerfield - Impact: Can't track repeat customers, no payment method saving
- Sessions created without
-
Missing Payment Intent Metadata
- Metadata only on session, not payment intent
- Impact: If session expires, metadata is lost
-
No Customer Email Pre-Collection
- Relies on Stripe Checkout form for email
- No pre-validation or prefill for returning customers
-
Missing Refund Reason Tracking
- No field to capture why customer cancelled
- No distinction between customer vs operator initiated refunds
-
Limited Webhook Event Handling
- Only handles
checkout.session.completed - Doesn't handle:
payment_intent.succeeded,charge.refunded,payment_intent.payment_failed
- Only handles
-
No Test Mode Detection
- Could accidentally use test keys in production
- No indication of test vs live mode in responses
-
No Stripe API Version Lock
- Stripe SDK initialized without API version
- Risk: Breaking changes when Stripe updates API
-
No Booking Status Field
- Google Sheets has reserved columns (K-M) but limited use
- No clear "confirmed", "pending", "cancelled" status tracking
-
Hard-coded Currency
- THB only (
/functions/api/checkout.ts:39) - No multi-currency support for expansion
- THB only (
-
No CORS Configuration
- May block legitimate cross-origin requests
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!
- 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
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:
routescollection: β PUBLIC READ ACCESS (17 documents)- All other collections: π Permission denied
{
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 | 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 |
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)
{
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
}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}`);
});Status: β OBSOLETE - We have direct Firestore access
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)
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!
- Fix hard-coded localhost URLs in checkout.ts
- Fix hard-coded ops email in cancel-booking.ts
- Add request validation to checkout endpoint
- Add try/catch to checkout request parsing
- Resolve refund logic inconsistency (choose one strategy)
- Add environment variable validation at startup
- Set up error monitoring (Sentry or CloudFlare Workers Analytics)
- Create availability API endpoint using Firebase SDK from
tests/ - Integrate real-time seat availability into booking flow
- Add pricing sync from Bundhaya routes collection
- Implement caching layer for Firebase queries (reduce load)
- Add monitoring for Firestore permission changes
- Build fallback if Firebase access is revoked
- Add idempotency keys to Stripe operations
- Implement webhook event logging
- Add duplicate booking prevention
- Implement rate limiting on API endpoints
- Add structured logging throughout
- Create Stripe customers for repeat bookings
- Handle additional webhook event types
- Add Stripe API version lock
- Build independent schedule/availability system
- Add multi-currency support
- Implement booking status tracking
- Add customer portal for booking management
- Set up comprehensive monitoring dashboard
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)β οΈ
OPS_EMAIL- Operations team email for refund notifications βSTRIPE_API_VERSION- Lock Stripe API version (e.g., "2023-10-16") β
| 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 |
- 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?)
- 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