```html

Implementing Payment Logging for Event Patrons: Lambda Handlers, DynamoDB Schema Updates, and CloudFront Routing Fixes

What Was Done

This session focused on building infrastructure to log patron payments for the ShipCaptainCrew event management system. The work involved three parallel streams: (1) adding Gmail credential retention to the Lambda environment, (2) implementing a payment logging modal and API handler in the dispatch HTML and Lambda function, and (3) fixing a CloudFront routing bug that was causing waiver page requests to fall through to the SPA instead of reaching Lambda.

Technical Details: Lambda Payment Handler Implementation

The core addition to /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py was a new handle_log_payment function that accepts POST requests to /api/events/{event_id}/payment. This handler:

  • Validates the request includes required fields: patron_name, amount, payment_method, and notes
  • Generates a unique payment ID using a timestamp + UUID suffix pattern for audit trails
  • Writes to the DynamoDB table (scc-events) using a composite key structure: the payment record is stored as a nested attribute under the event item to maintain transactional consistency
  • Returns a 200 response with the payment ID and timestamp on success, or 400/401/500 on validation/auth/service failures

The handler integrates with the existing auth layer by checking the Authorization header against ADMIN_PASS_HASH (stored in Lambda environment variables), ensuring only authenticated crew members can log payments. The pattern mirrors the existing handle_get_event and handle_list_events implementations for consistency.

Frontend Modal and API Integration

The dispatch HTML (/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html) received a new "Log Payment" modal component that:

  • Appears as a button within the event detail card (adjacent to existing edit/delete controls)
  • Renders as a Bootstrap-styled form with fields for patron name, amount (numeric input with 2 decimal places), payment method (dropdown: cash, card, venmo, check), and optional notes
  • Calls the apiFetch helper function (already present in the codebase) to POST to /api/events/{event_id}/payment with the crew member's auth token from localStorage
  • Displays success/error toasts using the existing banner infrastructure (the same notification system used for event create/update/delete confirmations)

The modal uses the same active-class-based visibility pattern as existing modals in the dispatch HTML, avoiding inline styles for consistency with the site's CSS architecture.

Infrastructure: CloudFront Routing Fix

During reconnaissance, a critical routing bug was identified: requests to /g/*/waiver were being routed to S3 (the origin for static assets) instead of the Lambda Function URL. This caused the SPA to receive HTML from the Lambda waiver handler, attempt to parse it as JSON, and fail with "Could not load event."

The fix involved adding a new CloudFront behavior in the shipcaptaincrew distribution:

  • Path Pattern: /g/*/waiver
  • Origin: Lambda Function URL (not S3)
  • Viewer Protocol Policy: Redirect HTTP to HTTPS
  • Cache Policy: Disabled (200ms Lambda execution time means caching waiver HTML is not beneficial for dynamic content)

This ensures requests like /g/2026-05-23/waiver reach the Lambda handle_waiver_get function (defined at line 1697 in lambda_function.py) which returns properly-formatted HTML that the browser renders directly, rather than the SPA trying to parse it.

DynamoDB Schema Considerations

The payment logging design stores payment records as a nested list attribute within the event item, rather than creating a separate payments table. This decision was made because:

  • Transactional consistency: Writing payment and event metadata in a single DynamoDB update ensures payment logs are never orphaned if an event is deleted
  • Query simplicity: Crew members typically fetch an event's details and need to see all associated payments in one call; a nested structure requires no joins
  • Scale appropriateness: Most events have fewer than 100 patrons, so a single-item payment list stays well under DynamoDB item size limits (400 KB)

The payment list is stored as event_item['payments'], a list of maps, each containing: payment_id, patron_name, amount, payment_method, notes, logged_by (crew member email), and logged_at (ISO 8601 timestamp).

Environment Variable Management

The Lambda function requires several new environment variables:

  • ADMIN_PASS_HASH — bcrypt hash of the crew password (used by all handlers for auth)
  • GMAIL_REFRESH_TOKEN, GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET — Google OAuth credentials for SES/email features

These were merged with existing env vars and deployed using the Lambda configuration API. The process involved pulling the current Lambda code and env vars as a snapshot, merging new vars, updating via AWS SDK, and waiting for the Lambda service to acknowledge the config update before deploying new code.

Deployment and Testing Flow

The workflow followed a staging-first approach:

  1. Local edits to lambda_function.py and index.html
  2. Build Lambda deployment zip from local sources and all dependencies
  3. Update Lambda environment variables with merged config
  4. Deploy updated Lambda code
  5. Push updated dispatch HTML to the _staging S3 prefix within the shipcaptaincrew bucket
  6. Invalidate /_staging/* on the CloudFront distribution to clear caches
  7. Smoke-test admin login, payment logging, and waiver routing on staging URLs

Staging validation confirmed: admin password validation works, the new payment endpoint returns 401 for invalid auth and 200 for valid requests, and waiver requests now reach Lambda instead of falling back to the SPA.

Key Decisions

  • Nested payments in events table: Simplifies queries and maintains transactional consistency; acceptable given typical event size
  • POST endpoint for payments: Idempotent design uses unique payment IDs so duplicate requests create duplicate payments (crew member must be careful); a future enhancement could add idempotency keys
  • CloudFront behavior ordering: The new /g/*/waiver behavior must appear before the catch-all /g/*