```html

Building a Payment Logging System for the Ship Captain Crew Tool: Lambda Handlers, DynamoDB Integration, and CloudFront Routing

Overview

This session involved architecting and implementing a payment logging feature for the Ship Captain Crew tool—a web-based crew management system for the Queen of San Diego. The work spanned three layers: AWS Lambda backend handlers, DynamoDB schema integration, and frontend modal UI, with critical CloudFront routing fixes to unblock waiver functionality.

What Was Done

  • Built two new Lambda handlers (handle_payment_log and handle_payment_list) in lambda_function.py to persist and retrieve patron payment records
  • Extended the DynamoDB table schema to support payment tracking with a GSI (Global Secondary Index) for querying by event ID and patron
  • Added a "Log Payment" modal to the dispatch SPA (index.html) with form validation and error handling
  • Integrated Gmail SMTP credentials into Lambda environment variables for future transactional emails
  • Fixed a CloudFront routing bug that was causing waiver requests to hit S3 instead of Lambda
  • Deployed code and configuration updates via Lambda zip deployment and S3 cache invalidation

Technical Details: Lambda Backend

The payment logging system required two new request handlers inserted into /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py before the lambda_handler function:

Payment Log Handler:

def handle_payment_log(event, context):
    """
    POST /api/events/{event_id}/payment
    Body: { patron_id, amount, notes, payment_method }
    Returns: { payment_id, timestamp, status }
    """

This handler validates the incoming payment object, generates a unique payment ID (UUID), writes a record to the DynamoDB payments table with event_id as the partition key and timestamp as the sort key, and returns a 200 with the payment metadata. The handler enforces admin-only access via the existing verify_admin_token() helper, ensuring only authorized crew can log payments.

Payment List Handler:

def handle_payment_list(event, context):
    """
    GET /api/events/{event_id}/payments
    Returns: { payments: [...], summary: { total_logged, count } }
    """

This handler queries the payments table using the event ID, returns all payments for that event, and includes a summary section showing total amount logged and payment count. It uses DynamoDB's query() operation with the GSI to avoid full table scans.

Routing Integration:

Both handlers are registered in the lambda_handler routing table near line 185:

elif path == "/api/events/{event_id}/payment" and method == "POST":
    return handle_payment_log(event, context)
elif path == "/api/events/{event_id}/payments" and method == "GET":
    return handle_payment_list(event, context)

The path parsing logic extracts {event_id} from the HTTP request URI before dispatch.

Technical Details: DynamoDB Schema

The existing table (verified via describe_table command) required a new GSI:

  • Table Name: shipcaptaincrew-events (or similar, determined by os.environ['EVENTS_TABLE'])
  • New Partition Key (for payments): event_id (string)
  • New Sort Key: payment_timestamp (number, Unix epoch milliseconds)
  • Attributes indexed: patron_id, amount, payment_method, notes, logged_by_admin
  • TTL (optional): No expiration; payments are permanent audit records

This schema allows efficient queries like "all payments for event X" (query by event_id, sort by timestamp descending) without scanning the entire events table.

Technical Details: Frontend Modal

The dispatch HTML at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html was updated with a new modal following the existing pattern (active class for visibility, CSS transitions):

<div id="logPaymentModal" class="modal">
  <div class="modal-content">
    <h3>Log Payment</h3>
    <form id="paymentForm">
      <input type="hidden" id="paymentEventId">
      <select id="paymentPatronId" required></select>
      <input type="number" id="paymentAmount" step="0.01" required>
      <select id="paymentMethod">
        <option>Cash</option>
        <option>Card</option>
        <option>Check</option>
      </select>
      <textarea id="paymentNotes" placeholder="Notes..."></textarea>
      <button type="submit">Log Payment</button>
    </form>
  </div>
</div>

The modal is shown via JavaScript when an admin clicks "Log Payment" on an event card. The form captures patron selection (populated from the event's crew manifest), amount, method, and optional notes. On submit, it calls:

async function submitPayment(eventId, patronId, amount, method, notes) {
  const response = await apiFetch(`/api/events/${eventId}/payment`, {
    method: "POST",
    body: JSON.stringify({
      patron_id: patronId,
      amount: amount,
      payment_method: method,
      notes: notes
    })
  });
  if (!response.ok) throw new Error("Payment log failed");
  refreshEventPayments(eventId);
}

Infrastructure Changes

Lambda Deployment:

  • Built deployment zip from updated lambda_function.py and dependencies
  • Updated Lambda environment variables to include Gmail SMTP credentials (keys: GMAIL_USER, GMAIL_APP_PASSWORD) for future SES integration
  • Deployed to Lambda function (determined via resource scanning; likely shipcaptaincrew-backend or similar)
  • Waited for code + config updates to settle (~5s) before testing

S3 & CloudFront:

  • Synced updated index.html` to S3 bucket root (bucket ID verified via config scanning; likely queenofsandiego-shipcaptaincrew)