```html

Building a Payment Logging System for Event Management: Lambda, DynamoDB, and SPA Integration

This post documents the design and implementation of a payment logging feature for the Ship Captain Crew event management system. The work involved integrating Gmail credential handling into a Lambda function, adding payment state management to a Single Page Application (SPA), and ensuring proper CloudFront routing for hybrid server-rendered and client-side content.

Problem Statement

The Ship Captain Crew tool (hosted at queenofsandiego.com/tools/shipcaptaincrew/) needed the ability to log payments for patrons attending charter events. The system had:

  • A DynamoDB table storing event data with patron rosters
  • A Lambda function handling API requests via CloudFront Function URL routing
  • A React-style SPA dispatch HTML serving as the primary UI
  • Partial payment infrastructure (banner, modal structure, auth checks) already in place

The gap: no handler to accept, validate, and persist payment records, and no UI component to trigger that flow.

Architecture Overview

The Ship Captain Crew stack consists of:

  • Lambda: /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py — routes HTTP requests, validates auth via ADMIN_PASS_HASH, queries/updates DynamoDB
  • DynamoDB: Schema includes event items with partition key event_id (format: YYYY-MM-DD-firstname-[period]) and nested patron rosters
  • SPA Dispatch HTML: queenofsandiego.com/tools/shipcaptaincrew/index.html (pulled from S3, currently 2463 lines after sync) — client-side routing, event fetching, UI rendering
  • CloudFront Distribution: Routes /api/* requests to Lambda Function URL; serves index.html for SPA routes
  • S3 Bucket: shipcaptaincrew — stores dispatch HTML and static assets

Technical Implementation

Lambda Enhancement: Payment Handler

The Lambda function already had helper functions for:

  • Admin authentication via ADMIN_PASS_HASH comparison
  • SES email dispatch
  • DynamoDB queries with error handling

We added a new payment handler following the existing routing pattern:

def handle_payment_log(event, context):
    """
    POST /api/events/{eid}/patron/{pid}/payment
    Logs a payment for a patron, updates DynamoDB, 
    and sends confirmation email.
    """
    # 1. Extract and validate path parameters
    # 2. Verify admin auth from headers
    # 3. Parse JSON body (amount, method, notes)
    # 4. Update patron payment_cleared field in DynamoDB
    # 5. Send SES confirmation to admin
    # 6. Return 200 with updated patron record

This handler was inserted before the main lambda_handler routing dispatcher (around line 1750 in the development version), following the same pattern as existing handlers like handle_get_event and handle_list_events.

Routing and Path Parsing

The Lambda router uses regex matching on the request path:

path = event['rawPath']  # e.g., /api/events/2026-05-23-john/patron/john-smith/payment

if path.startswith('/api/events/') and path.endswith('/payment'):
    return handle_payment_log(event, context)

This pattern avoids conflicts with existing routes and maintains consistency with the codebase.

SPA Frontend: Payment Modal

The dispatch HTML already had modal infrastructure (active class toggling). We added a "Log Payment" modal following the same pattern:

  • Modal trigger: Button in patron card row, visible only to authenticated admins
  • Form fields: Amount (float), payment method (dropdown: cash/check/venmo/stripe), optional notes
  • Submission: Uses existing apiFetch helper to POST to /api/events/{eid}/patron/{pid}/payment
  • Error handling: Catch block displays user-friendly error (e.g., "Could not log payment: network error")
  • Success handling: Refresh event data, close modal, show success banner

The modal HTML was inserted after the existing event-detail modal, around line 1400 in the dispatch HTML.

Infrastructure and Deployment

Environment Variables

The Lambda function environment includes credentials for SES and Gmail token refresh (if applicable). We verified these were present before deploying:

aws lambda get-function-configuration \
  --function-name shipcaptaincrew \
  --query 'Environment.Variables' \
  --region us-west-2

The merged env payload was pushed to preserve existing variables while adding/updating payment-related config.

Deployment Workflow

  1. Lambda Code: Snapshot prod zip, apply patches locally, syntax-check Python, build new zip, deploy via AWS API
  2. Lambda Config: Merge env vars, push to Lambda, wait for config update to settle (typically <5 seconds)
  3. SPA HTML: Sync from S3 (to catch any upstream changes), apply patches, deploy to S3, invalidate CloudFront cache at /*
  4. Staging validation: Deploy to /_staging/ path, test with real admin login, verify API responses before production rollout

CloudFront Routing (Known Issue)

During development, we discovered a routing gap: requests to /g/{event_id}/waiver were falling through to the SPA instead of reaching the Lambda waiver handler. The fix (not yet implemented) requires adding a new CloudFront behavior:

  • Path pattern: /g/*/waiver
  • Origin: Lambda Function URL
  • Priority: Higher than the default SPA catch-all

This is a broader infrastructure cleanup task tracked separately.

Key Decisions

  • Handler placement in Lambda: Inserted before lambda_handler to follow the existing pattern and keep related logic grouped
  • Modal pattern reuse: Used existing HTML modal structure and CSS classes to minimize new code and maintain consistency
  • Admin-only visibility: Payment logging is gated by auth headers and frontend UI visibility checks, preventing unauthorized access
  • Staging slot testing: Deployed to /_staging/ path to validate changes before production, using real admin credentials for smoke tests
  • SES for confirmation: