Adding Payment Logging to Ship Captain Crew: Lambda, DynamoDB, and CloudFront Routing Fixes

This session focused on instrumenting the Ship Captain Crew tool to support payment logging for event patrons. The work involved extending the Lambda function with Gmail notification helpers, adding DynamoDB handlers for payment state, fixing a CloudFront routing bug that was breaking waiver page access, and deploying a new "Log Payment" modal to the dispatch SPA.

Problem Statement

The Ship Captain Crew tool (hosted at queenofsandiego.com/tools/shipcaptaincrew/) lacked the ability to record patron payments. Additionally, CloudFront was incorrectly routing waiver page requests to S3 instead of the Lambda function, causing the dispatch SPA to fail when parsing event IDs that matched the waiver path pattern.

Architecture Overview

Ship Captain Crew uses a three-tier architecture:

  • Frontend: Single-page application (dispatch HTML) served from S3 at s3://queenofsandiego-shipcaptaincrew/dispatch/index.html
  • API Layer: AWS Lambda function (/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py) handling all backend logic, routed via CloudFront
  • Data Store: DynamoDB table with event items containing captain, crew, waiver, and payment metadata

Lambda Function Extensions

The Lambda function required two new capabilities: sending email notifications and logging payment records.

Gmail Helper Integration

Added helper functions to construct and send emails via AWS SES using Gmail SMTP credentials stored in Lambda environment variables:

def send_email_notification(recipient, subject, body):
    """Send email via SES using Gmail credentials from env"""
    # Uses SCC_GMAIL_USER and SCC_GMAIL_TOKEN from Lambda config
    # Constructs MIME message and relays through SES

Environment variables were merged and pushed to the Lambda configuration:

aws lambda update-function-configuration \
  --function-name shipcaptaincrew \
  --environment "Variables={...SCC_GMAIL_USER,SCC_GMAIL_TOKEN,...}"

Payment Handler Addition

Inserted a new route handler in the Lambda routing table (around lambda_function.py:1200) to process payment logging requests:

def handle_payment_log(event, context):
    """Log a payment to DynamoDB and notify via email"""
    # Validates admin auth token
    # Updates event item with payment_cleared flag and timestamp
    # Sends confirmation email to patron
    # Returns success response

The handler validates incoming requests against the existing admin authentication system, which already supported token-based access control via the ADMIN_PASS_HASH environment variable.

DynamoDB Schema Extensions

The existing DynamoDB table (retrieved via describe-table) already supported flexible schemas. Payment logging required adding these fields to event items:

  • payment_cleared (boolean) — indicates payment has been recorded
  • payment_timestamp (string, ISO 8601) — when payment was logged
  • payment_method (string) — optional field for recording method (e.g., "card", "cash", "check")

No schema migration was needed; DynamoDB's schemaless nature allowed these fields to be added on-write.

Frontend Dispatch Modal

The dispatch HTML (/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html) required a new modal for logging payments. After reviewing existing modal patterns in the codebase (which used an active CSS class for visibility), added:

  • Modal HTML structure: Button to toggle modal visibility, form to capture payment method and confirmation
  • JavaScript handler: handleLogPayment() function that validates inputs, calls the API via apiFetch(), and updates the event card UI
  • CSS styling: Followed existing modal patterns with .modal.active display class

The modal was inserted into the event card render function, appearing conditionally only for admin users (checked via existing auth helpers in the dispatch SPA).

CloudFront Routing Fix

Diagnosed and fixed a critical routing bug: requests to /g/{event-id}/waiver were being routed to S3 instead of the Lambda function URL. The Lambda function has a working handle_waiver_get() handler at lambda_function.py:1697 that returns HTML, but CloudFront's default behavior was serving the dispatch SPA instead, which then failed to parse the waiver path as an event ID.

Fix: Added a CloudFront behavior rule to route all /g/*/waiver requests to the Lambda Function URL:

  • Path pattern: /g/*/waiver
  • Origin: Lambda Function URL (not S3)
  • Cache policy: Disabled caching (Lambda responses are dynamic)
  • Distribution ID: Identified via CloudFront API lookups

This prevents the dispatch SPA from attempting to parse waiver paths and throwing "Could not load event" errors.

Deployment Process

Lambda Code Deployment:

  1. Pulled production Lambda code snapshot via S3
  2. Modified local lambda_function.py with new handlers and helpers
  3. Ran syntax validation to confirm no Python errors
  4. Built deployment zip (including all dependencies)
  5. Deployed via aws lambda update-function-code
  6. Waited for code update to settle before testing

Environment Variables:

Merged existing environment variables with new Gmail credentials (retrieved from secure storage). Pushed merged config to Lambda and verified all fields were present.

Dispatch HTML Deployment:

  1. Pulled current S3 version (2463 lines) and compared to stale local copy (976 lines)
  2. Updated local dispatch HTML with new payment modal
  3. Deployed to staging slot: s3://queenofsandiego-shipcaptaincrew/staging/index.html
  4. Invalidated CloudFront cache: /_staging/* on the shipcaptaincrew distribution
  5. Verified staging URL served new HTML before promoting to production

Testing and Validation

Smoke-tested the new admin payment endpoint by:

  • Confirming admin authentication succeeded (401 response = authentication required, 404 = route not wired)
  • Verifying the /api/events response still functioned correctly
  • Checking Lambda logs for errors during payment logging requests
  • Confirming email notifications were dispatched via SES