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}/waiverwere 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(checkinglambda_handlerdispatch 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/*/waivermisconfiguration)
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">×</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:
- Lambda Environment Variables: Updated with merged configuration (preserving all existing Gmail credentials and admin hash)
- Lambda Code: Deployed the updated
lambda_function.pywith new payment handler - CloudFront Behavior: Added the
/g/*/waiverbehavior with Lambda origin - Dispatch HTML: