Implementing Patron Payment Logging in the Ship Captain Crew Tool: Lambda Event Handlers, HTML Modals, and CloudFront Routing
What Was Done
This session focused on adding patron payment logging functionality to the Ship Captain Crew (SCC) management tool. The work involved three major components: (1) extending the Lambda function with Gmail integration and payment event handlers, (2) adding a modal UI to the dispatch HTML for logging payments, and (3) diagnosing and documenting a CloudFront routing issue affecting waiver page loads.
Technical Details: Lambda Handler Architecture
The SCC Lambda function (/tools/shipcaptaincrew/lambda_function.py) uses an event-routing pattern where the HTTP path determines which handler executes. The existing structure routes requests like /api/events, /api/event/{id}, and /g/{event_id}/waiver to corresponding handler functions.
To support payment logging, we extended this architecture by:
- Adding Gmail helper functions before the main
lambda_handlerto compose and send notification emails when a payment is logged. These helpers extract Gmail API credentials from Lambda environment variables and format plaintext + HTML email bodies. - Implementing a
handle_payment_loghandler that accepts POST requests to/api/payment/log. This handler validates the incoming payment record (patron name, amount, event ID, timestamp), writes it to the DynamoDB table (resource:scc-events), and triggers a Gmail notification to the admin. - Routing payment requests in
lambda_handlerby checking the path prefix and delegating to the new handler when appropriate.
The Lambda function also required environment variable updates to persist Gmail OAuth2 credentials (refresh token, client ID, client secret). These were merged with existing env vars and deployed atomically using the AWS Lambda API.
Frontend: Modal UI for Payment Entry
The dispatch HTML (/tools/shipcaptaincrew/index.html) already contained infrastructure for modals (banner notifications, form overlays). We added a "Log Payment" modal following the existing DOM pattern:
- A hidden modal container with CSS class
activetoggled via JavaScript - Form fields for patron name, payment amount, event selection, and payment method
- Submit button that calls
apiFetch('/api/payment/log', { method: 'POST', body: ... }) - Error/success feedback via the existing banner system
The modal is triggered by a new button in the crew dashboard, accessible only to authenticated admins. The form data is serialized to JSON and sent to the Lambda endpoint, which validates, persists, and notifies.
Infrastructure Changes
DynamoDB Table Schema: The existing scc-events table (partition key: event_id, sort key: timestamp) was examined to confirm it can store payment records as nested objects within event items. Payment data is stored as an attribute payment_log (list of maps) rather than creating a separate table, keeping the schema simple and payment tied to event context.
Lambda Deployment: The updated function was packaged and deployed to the SCC Lambda function URL. Environment variables (including Gmail credentials) were merged with existing vars and pushed via the Lambda API. The deployment was monitored using CloudWatch Logs to confirm no regressions.
S3 & CloudFront: The dispatch HTML was deployed to the S3 bucket (scc-dispatch) and invalidated on the CloudFront distribution to ensure immediate cache refresh. The distribution ID and behavior paths were verified to confirm routing.
Diagnosed Issue: CloudFront Waiver Route Routing
During reconnaissance, we identified a routing bug affecting waiver page loads (e.g., /g/2026-05-23/waiver/g/*/waiver requests to S3 (the dispatch SPA), not to the Lambda function. The SPA then mis-parses the slug as an event ID, fetches /api/g/2026-05-23/waiver, receives HTML from Lambda, and crashes when calling r.json().
Root cause: The CloudFront distribution lacks a behavior rule for /g/*/waiver pointing to the Lambda Function URL origin.
Fix (not yet deployed): Add a CloudFront behavior with path pattern /g/*/waiver and set the origin to the Lambda Function URL, with cache disabled. This ensures waiver requests bypass S3 and route directly to the Lambda handler at handle_waiver_get (line 1697 in lambda_function.py).
Secondary observation: Event ID slug 2026-05-23 violates the intended YYYY-MM-DD-firstname-[period] naming convention, suggesting data quality issues upstream.
Key Decisions
- Event-routing over separate Lambda functions: We extended the existing monolithic Lambda handler rather than creating separate functions for payment logging. This reduces operational overhead (fewer cold starts, simpler IAM) and keeps payment logic colocated with event management.
- Gmail over SES: Although the infrastructure already uses AWS SES for some notifications, Gmail was chosen here because it integrates with existing team workflows and email client filters. The OAuth2 credentials are stored as encrypted environment variables on Lambda.
- DynamoDB schema: nested payment records: Rather than splitting payments into a separate table with a foreign key, payment logs are stored as lists within event items. This avoids joins and keeps related data atomic.
- CloudFront routing diagnosis, not deployment: The waiver routing issue was identified but not fixed in this session, pending confirmation from the team. This prevents cascading changes and ensures the fix is tested separately.
Verification & Testing
The Lambda function was syntax-checked and compared against the production snapshot to ensure no unintended regressions. The dispatch HTML was deployed to a staging slot (/_staging/*) and verified to serve correctly. Admin login was smoke-tested using real credentials to confirm the payment modal is reachable.
Recent CloudWatch Logs were checked for errors related to payment route handling and Gmail integration.
What's Next
Once this session is ready, the team lead will receive an email notification. At that point, payment logging can be tested end-to-end with real patron records. The CloudFront waiver routing fix should be deployed as a separate, tracked task to avoid scope creep.