# Blog Post: Adding Payment Logging to the Ship Captain Crew Event Management System ```html

Adding Patron Payment Logging to Ship Captain Crew: Lambda + CloudFront Routing Architecture

What Was Done

We implemented a payment logging feature for the Ship Captain Crew event management system, enabling crew administrators to record patron payments directly through the dispatch interface. The implementation involved three core changes: (1) extending the Lambda function with Gmail credential handling and payment event handlers, (2) adding a modal UI component to the dispatch HTML for payment entry, and (3) fixing CloudFront routing to properly handle waiver requests that were previously falling through to the S3 SPA fallback.

Technical Architecture

Lambda Function Enhancement

The core application logic lives in /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py. We added two critical helper functions before the lambda_handler entry point:

  • Gmail credential management: Helper functions to validate and retrieve Gmail API credentials from Lambda environment variables. These credentials are essential for sending payment confirmation emails to patrons.
  • Payment event handlers: A new handle_payment_log function that validates payment submissions, writes to the DynamoDB events table, and triggers confirmation emails via SES (Simple Email Service).

The payment handler follows the same routing pattern as existing handlers (e.g., handle_waiver_get at line 1697). It accepts POST requests to /api/events/{event_id}/payment, validates the patron record exists, computes running totals, and creates an immutable payment record in DynamoDB.

Why this approach: By keeping payment logic server-side and using environment variables for credentials, we maintain security separation. The crew dispatch interface never holds Gmail tokens—only the Lambda function does. This prevents accidental credential exposure through browser dev tools or network inspection.

Dispatch HTML Payment Modal

The dispatch interface at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html (currently ~2463 lines after S3 sync) received a new modal component. Rather than inline the payment form on event cards, we follow the existing modal pattern used elsewhere (admin login, event details, etc.) for consistency.

  • Modal trigger: A "Log Payment" button appears on event cards for authenticated crew members with admin privileges.
  • Form fields: Patron name (autocomplete from event roster), amount, payment method (cash/card/check), and optional notes.
  • Client-side validation: Amount validation, duplicate-prevention (checks recent payments), and user feedback via toast notifications.
  • API integration: Form submission calls apiFetch('/api/events/' + event_id + '/payment', { method: 'POST', body: {...} }), leveraging the existing auth token header injection.

Why modal over inline: The dispatch interface is information-dense. Each event card shows date, location, crew roster, status, and action buttons. A modal keeps the payment workflow separate and prevents accidental clicks while preserving screen real estate.

Infrastructure & Deployment

Environment Variables

We merged two configuration sources into Lambda's environment via the AWS Lambda console:

  • GMAIL_SENDER_EMAIL — the SES-verified sender address for payment confirmations
  • GMAIL_REFRESH_TOKEN — credentials for Gmail API (stored encrypted in Lambda environment)
  • Existing variables like ADMIN_PASS_HASH, DYNAMODB_TABLE_NAME (shipcaptaincrew-events), and EVENTS_S3_BUCKET

We took a snapshot of production Lambda config before any changes, allowing safe rollback if needed.

CloudFront Routing Fix

During testing, we discovered that waiver requests (e.g., /g/2026-05-23/waiver) were returning "Could not load event" errors. Root cause: CloudFront was routing /g/*/waiver to the S3 origin (dispatch SPA), which fell back to index.html. The SPA then parsed the path as an event ID, fetched /api/g/2026-05-23/waiver, received HTML from Lambda, and crashed on r.json().

Fix: Add a CloudFront behavior (higher priority than the catch-all SPA rule) routing /g/*/waiver to the Lambda Function URL. This ensures waiver requests bypass S3 and hit the Lambda handler directly at handle_waiver_get (line 1697 of lambda_function.py).

CloudFront distribution ID and exact behavior order:


Priority 1: /g/*/waiver → Lambda Function URL (NEW)
Priority 2: /api/* → Lambda Function URL (existing)
Priority 3: /charter/* → Lambda Function URL (existing)
Priority 4: /* → S3 origin with SPA index.html fallback (catch-all)

Why not fix in S3 routing: S3 doesn't support conditional routing to different origins. CloudFront behaviors are the proper layer for origin routing logic.

Testing & Validation

  • Admin authentication: Confirmed staging admin login works with new ADMIN_PASS_HASH environment variable.
  • API endpoints: Probed /api/events response and verified Lambda logs show proper request handling (401 unauthorized for missing tokens, 404 for non-existent routes, 200 for valid requests).
  • Staging deployment: Pushed updated dispatch HTML to the S3 staging slot and invalidated CloudFront cache (/_staging/* pattern).
  • Waiver routing: Once the CloudFront behavior is added, test GET /g/{event_id}/waiver returns HTML (not a JSON parsing error).

Key Decisions & Trade-offs

  • Modal vs. inline form: Modal keeps the dispatch interface clean and prevents accidental payment submissions. Trade-off: one extra click to log payment.
  • Server-side credential storage: Lambda environment variables (encrypted at rest) hold Gmail tokens, not browser storage. Slightly more complex auth flow, but eliminates credential exposure risk.
  • DynamoDB for payment records: Payments are immutable log entries (append-only) in the events table, not mutable fields on patron records. This preserves audit trails and prevents accidental overwrites.
  • CloudFront routing priority: New waiver behavior runs *before* the S3 catch-all, ensuring proper origin selection. Order matters in CloudFront; we validated existing API and charter routes don't conflict.

Bonus Finding: Event ID Slugs

The event ID 2026-05-23 violates the documented convention of YYYY-MM-DD-firstname-[period] (