Implementing Payment Logging for Patron Events: Lambda + CloudFront + DynamoDB Integration
What Was Done
This session implemented a complete payment logging workflow for the Ship Captain Crew tool, enabling administrators to record patron payments against scheduled events. The implementation spans three layers: a new modal UI component in the dispatch SPA, Lambda backend handlers for payment persistence, and infrastructure routing fixes to support waiver fulfillment.
Problem Context
The Ship Captain Crew tool (hosted at shipcaptaincrew.queenofsandiego.com) manages event bookings and crew assignments. Previously, there was no mechanism to log when a patron paid for their event slot. Additionally, a routing misconfiguration was causing waiver requests (/g/{event_id}/waiver) to fall through to the dispatch SPA instead of being handled by the Lambda function.
Technical Details: Backend Implementation
Payment Handler Architecture
The Lambda function at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py required two new handlers:
handle_payment_log()— POST endpoint that records a payment event to DynamoDBhandle_payment_cleared()— GET endpoint that retrieves payment status for a given event and patron
The handlers follow the existing pattern in the Lambda: they parse the event object (AWS Lambda event, not a ship event), extract path parameters and body payloads, validate against the DynamoDB events table schema, and return JSON responses.
DynamoDB Schema Extension
The existing events table uses a composite key: PK = eid (event ID in YYYY-MM-DD-firstname format) and SK = crew_id (unique crew assignment identifier). Payment logging required a new attribute:
payments(Map) — keyed by patron ID, containingamount,timestamp,method(e.g., "cash", "venmo"), andlogged_by(admin user ID)
This design avoids a separate table and keeps payment records co-located with event data for efficient queries.
Gmail Token Retention
A critical decision: preserve existing Gmail credentials in Lambda environment variables across deploys. Prior sessions had established SES integration for admin notifications; this session ensured GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, and GMAIL_REFRESH_TOKEN remained in the merged env vars payload sent to Lambda during the update.
Technical Details: Frontend Implementation
Modal UI Pattern
The dispatch HTML at index.html required a new "Log Payment" modal following the existing pattern. The codebase uses a simple active-class toggle for visibility (no external modal library), so the new modal mirrors the structure of existing modals with:
- Modal container with
id="log-payment-modal"and class-based visibility toggle - Form fields: patron ID selector, amount input, payment method dropdown, logged-by (pre-filled with admin token claims)
- Submit button wired to a new
submitPaymentLog()function
API Integration
The dispatch HTML's apiFetch() helper handles authentication via Bearer token (stored in window.adminToken). Payment submission calls:
POST /api/events/{eid}/payment-log
Authorization: Bearer {adminToken}
Content-Type: application/json
{
"patron_id": "...",
"amount": 125.00,
"method": "venmo",
"notes": "..."
}
On success, the modal closes and the event card updates to reflect the new payment record.
Infrastructure: CloudFront Routing Fix
Waiver Route Misconfiguration
Analysis revealed that requests to /g/2026-05-23/waiver were routing to S3 instead of the Lambda function. Root cause: CloudFront distribution behaviors were not configured to route /g/*/waiver paths to the Lambda Function URL.
The fix adds a new CloudFront behavior:
- Path Pattern:
/g/*/waiver - Origin: Lambda Function URL (not S3)
- Cache Settings: TTL = 0 (no caching for dynamic handler responses)
- Compress: Enabled
This ensures waivers bypass the dispatch SPA and hit the Lambda handler at line 1697 (handle_waiver_get()), which returns HTML directly.
Event ID Slug Convention
A secondary issue: the event ID "2026-05-23" violates the established convention of YYYY-MM-DD-firstname-[period] (e.g., "2026-05-23-captain-cory-afternoon"). This causes the dispatch SPA parser to misinterpret the waiver path. Correcting event IDs in the roster will prevent similar issues.
Deployment Process
Lambda Update Workflow
The Lambda deployment follows a careful sequence to avoid credential loss:
- Snapshot current Lambda code (zip) and env vars
- Build local Lambda deploy zip with new handlers
- Merge local env vars with production env vars (preserving Gmail tokens)
- Push merged env vars to Lambda configuration
- Wait for config update to settle (CloudWatch confirms)
- Deploy updated Lambda code
- Wait for code update to settle
- Smoke-test new endpoints (admin auth)
Staging Validation
HTML changes deployed to the CloudFront staging slot (/_staging/) before production. CloudFront cache invalidation ensured fresh content was served immediately:
aws cloudfront create-invalidation \
--distribution-id {DIST_ID} \
--paths "/_staging/*"
Admin login was verified against the new endpoint before final production promotion.
Key Decisions
- Single DynamoDB Table: Payments stored as a nested map in the events table rather than a separate payments table. Reduces complexity, keeps related data together, and avoids additional round-trips.
- Bearer Token Auth: Reused existing admin token mechanism. Payment logging is an admin-only operation; token claims provide the
logged_byaudit trail. - Modal over Page: In-app modal for payment entry keeps crew in the event management context. No page redirect, faster workflow.
- CloudFront Behavior Specificity: Adding a dedicated
/g/*/waiverbehavior isolates waiver routes from the default SPA catch-all, preventing mis-parsing.
What's Next
The payment logging feature is now staged and ready for admin testing. The next steps are:
- Smoke test