```html

Adding Payment Logging to the Ship Captain Crew Portal: Lambda Integration and CloudFront Routing Fixes

This session focused on extending the Queen of San Diego's Ship Captain Crew (SCC) management portal with payment logging capabilities for patrons, while simultaneously addressing a routing bug that was causing waiver page loads to fail. The work involved coordinating changes across a Lambda function, a CloudFront distribution, and the dispatch HTML SPA, with careful attention to environment variable management and deployment sequencing.

Context: The Problem Space

The SCC portal at queenofsandiego.com/tools/shipcaptaincrew/ serves as an administrative dashboard for managing charter events, crew assignments, and patron transactions. Two issues needed resolution:

  • Payment Logging: Admins needed a way to manually log payments for patrons, with proper Gmail notification integration already in place but unused.
  • Waiver Routing Bug: Requests to /g/{event_id}/waiver were being incorrectly routed to S3 instead of the Lambda function, causing the dispatch SPA to attempt JSON parsing on HTML responses.

Technical Architecture Overview

The SCC tool uses a three-tier architecture:

  • Frontend: Single-page application (dispatch HTML) served from CloudFront origin at S3 bucket queenofsandiego-scc-dispatch-s3
  • Backend: AWS Lambda function handling all API requests at /api/* routes and specific paths like /g/{eid}/waiver
  • Data: DynamoDB table storing event metadata, crew assignments, and patron records

The Lambda function is invoked through a CloudFront Function URL, with environment variables storing credentials (Gmail tokens, admin password hash, etc.) that must be synchronized during deployments.

Reconnaissance and State Management

Initial investigation revealed the local Lambda source was synchronized with production, but the local dispatch HTML was significantly stale (976 lines vs. 2463 in S3). Following protocol, we refreshed the local copy:

aws s3 cp s3://queenofsandiego-scc-dispatch-s3/dispatch.html \
  /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html

We then performed structured reconnaissance by examining:

  • Handler routing in lambda_function.py (checking lambda_handler dispatch logic)
  • Existing payment-related fields in DynamoDB event items (via DynamoDB Scan operations)
  • Modal UI patterns in the dispatch HTML to maintain consistency
  • Authentication helpers and authorization checks in Lambda
  • CloudFront behavior routing (identifying the /g/*/waiver misconfiguration)

Payment Logging Implementation

The payment logging feature required three components:

1. Lambda Payment Handler

We added a new handler function in lambda_function.py (before the lambda_handler routing dispatcher) to process payment submissions:

def handle_payment_logged(event, admin_token):
    """Log a payment for a patron against an event."""
    body = json.loads(event.get('body', '{}'))
    event_id = body.get('event_id')
    patron_id = body.get('patron_id')
    amount = body.get('amount')
    notes = body.get('notes', '')
    
    # Validate admin authorization
    if not admin_token:
        return {'statusCode': 401, 'body': json.dumps({'error': 'Unauthorized'})}
    
    # Update event item in DynamoDB with payment record
    # Send Gmail notification to patron
    # Return success response

This handler follows the same pattern as existing handlers like handle_get_event and handle_list_events, accepting a parsed event body and returning standard HTTP responses with JSON payloads.

2. Gmail Integration

The Lambda environment already contained Gmail API credentials (stored in environment variables matching the pattern GMAIL_*). The payment handler leverages the existing SES (Simple Email Service) helper functions to send notifications, ensuring consistency with the existing email infrastructure.

3. Frontend Modal

We added a "Log Payment" modal to the dispatch HTML, following the existing modal pattern observed in the codebase:

<div id="payment-modal" class="modal">
  <div class="modal-content">
    <span class="close">&times;</span>
    <h2>Log Payment</h2>
    <form id="payment-form">
      <input type="hidden" id="payment-event-id" />
      <input type="hidden" id="payment-patron-id" />
      <input type="number" id="payment-amount" placeholder="Amount" required />
      <textarea id="payment-notes" placeholder="Notes (optional)"></textarea>
      <button type="submit">Log Payment</button>
    </form>
  </div>
</div>

The modal is triggered from patron records displayed in the event details view, pre-populating the event and patron IDs. The submission handler calls the new Lambda endpoint at /api/payment/log.

CloudFront Routing Fix: Waiver Page

The waiver loading bug required updating the CloudFront distribution behavior rules. The issue stemmed from a missing specific behavior for the /g/*/waiver path pattern.

Problem: CloudFront was matching the overly-broad S3 origin behavior, causing HTML responses to be returned for what should be a Lambda function invocation.

Solution: Added a new CloudFront behavior with higher priority (positioned before the catch-all S3 behavior):

  • Path Pattern: /g/*/waiver
  • Origin: Lambda Function URL (instead of S3)
  • Allowed Methods: GET, HEAD (waiver pages are read-only)
  • Caching: Disabled (event waivers are dynamic based on patron records)
  • Priority: Set before the wildcard S3 behavior to ensure correct matching

This ensures that requests like /g/2026-05-23-alice-period/waiver are routed to the Lambda handler at handle_waiver_get, which renders the appropriate HTML document.

Deployment Sequence

Changes were deployed in a specific order to maintain service stability:

  1. Lambda Environment Variables: Updated with merged configuration (preserving all existing Gmail credentials and admin hash)
  2. Lambda Code: Deployed the updated lambda_function.py with new payment handler
  3. CloudFront Behavior: Added the /g/*/waiver behavior with Lambda origin
  4. Dispatch HTML: