```html

Adding Payment Logging Infrastructure to Ship Captain Crew: Lambda Handlers, Gmail Integration, and CloudFront Routing Fixes

This post documents the implementation of payment logging functionality for the Ship Captain Crew patron management tool, including new Lambda request handlers, Gmail token retention across deployments, CloudFront behavior additions, and a modal-based UI for logging payments in the dispatch SPA.

Problem Statement & Architecture Overview

The Ship Captain Crew tool needed a way to log patron payments directly from the crew dashboard without leaving the application context. The existing infrastructure included:

  • A Lambda function at /tools/shipcaptaincrew/lambda_function.py handling crew operations
  • A dispatch SPA in /tools/shipcaptaincrew/index.html served from S3 with CloudFront caching
  • A DynamoDB table storing event and crew data
  • Existing admin authentication via token validation

The challenge was multi-faceted: design new Lambda handlers for payment operations, preserve sensitive credentials (Gmail tokens) across Lambda deployments, fix CloudFront routing for nested resource paths (like /g/*/waiver), and build a user-friendly modal interface in the SPA.

Lambda Implementation: Handlers & Gmail Token Persistence

New payment-related handlers were added to lambda_function.py before the main lambda_handler routing logic. The pattern followed existing conventions in the codebase:

def handle_log_payment(event, context):
    """
    POST /api/log-payment
    Validates admin auth, logs payment to DDB, returns confirmation.
    """
    # Extract auth token, validate against roster
    # Parse request body: {event_id, crew_id, amount, notes}
    # Write to DDB payment_cleared field or dedicated payment table
    # Return 200 with confirmation or 400 with validation error

A critical discovery during development: Lambda environment variables persist across code deployments but are wiped if you redeploy without including them in the update payload. Gmail OAuth tokens and other credentials were stored in env vars. The fix involved:

  1. Reading current Lambda environment via AWS CLI: aws lambda get-function-configuration --function-name scc
  2. Extracting the existing env var block (specifically Gmail creds like GMAIL_TOKEN, GMAIL_REFRESH_TOKEN)
  3. Merging them with new env vars before deployment: aws lambda update-function-configuration --environment Variables={...merged...}
  4. Deploying code separately: aws lambda update-function-code --zip-file fileb://lambda.zip

This two-step process ensured credentials weren't lost during code pushes. The deploy sequence was:


# 1. Build new Lambda code
zip -r lambda.zip lambda_function.py helpers/
# 2. Merge current env vars with new ones (Gmail creds retained)
aws lambda update-function-configuration \
  --function-name scc \
  --environment Variables={...GMAIL_TOKEN=...,NEW_VAR=...}
# 3. Wait for config to settle (~5 seconds)
sleep 5
# 4. Deploy code
aws lambda update-function-code \
  --function-name scc \
  --zip-file fileb://lambda.zip

CloudFront Routing: Fixing Nested Resource Paths

During testing, accessing /g/2026-05-23/waiver was returning "Could not load event" errors. Root cause analysis revealed a routing ambiguity:

  • Lambda had a working handler at /g/{eid}/waiver that returns HTML (line 1697 in lambda_function.py)
  • CloudFront's default behavior was routing /g/* to S3 instead of Lambda
  • The S3 fallback triggered the dispatch SPA, which mis-parsed 2026-05-23/waiver as an event_id
  • The SPA made a request to /api/g/2026-05-23/waiver, received HTML instead of JSON, and r.json() threw an exception

The fix required a new CloudFront behavior (higher priority than the catch-all /g/*` → S3):


CloudFront Distribution: scc-shipboard-cloudfront
New Behavior:
  Path Pattern: /g/*/waiver
  Origin: scc-lambda-function-url (not S3)
  Allowed Methods: GET, POST, HEAD, OPTIONS
  Cache Policy: Managed-CachingDisabled
  Origin Request Policy: AllViewerExcept CloudFront-Authorization
  Function Associations: None needed

The key principle: more specific path patterns must be listed before catch-all patterns in CloudFront behaviors. The order after the change was:

  1. /g/*/waiver → Lambda
  2. /api/* → Lambda
  3. /g/* → S3 (existing)
  4. /* → S3 (default, falls through to SPA)

After deployment, the distribution cache was invalidated: aws cloudfront create-invalidation --distribution-id [ID] --paths "/g/*". A secondary issue noted (but not fixed in this session): event slug 2026-05-23 violates the expected YYYY-MM-DD-firstname-[period] convention, which should be addressed in data cleanup.

Dispatch SPA: Payment Logging Modal

The frontend required a modal form matching the existing UI patterns. Key locations in index.html:

  • Modal structure follows existing patterns (class toggle for visibility, backdrop dismissal)
  • Modal is appended near other modals (around line 1200+)
  • Form fields: input[name="crew_id"], input[name="amount"], textarea[name="notes"]
  • Submit handler calls apiFetch('/api/log-payment', 'POST', {...})
  • Success closes modal and refreshes event list via loadEvents()

The modal trigger was added to event card rendering logic, visible only to authenticated crew members with admin role. The apiFetch helper (existing in the SPA) handles auth token injection and error handling.

Key Architectural Decisions

  • Env Var Merging Over Secrets Manager: Gmail tokens in Lambda env vars rather than AWS Secrets Manager to avoid additional API calls and cold-start latency. Trade-off: credentials visible in Lambda config (mitigated by IAM-restricted access).
  • Handler Pattern Consistency: Payment handlers follow the same routing + validation pattern as existing handlers (e.g., handle_get_event), making the codebase predictable for future maintainers.
  • CloudFront Behavior Specificity: New behavior for /g/*/waiver rather than a catch-all avoids unintended routing of other S3-backed paths to Lambda.
  • Modal Over Page Navigation: In-page modal keeps users in the crew dashboard context