Integrating Payment Logging into the Ship Captain Crew Event Management Tool
What Was Done
We extended the Ship Captain Crew (SCC) event management platform to support payment logging for patrons attending chartered events. This involved adding a new admin-only modal interface to the dispatch SPA, implementing backend payment handlers in the Lambda function, integrating Gmail credential storage for audit logging, and deploying infrastructure changes across CloudFront and Lambda environment configuration.
Technical Details
Frontend: Payment Modal and Event Card Integration
The dispatch HTML file (/tools/shipcaptaincrew/index.html) required a new modal dialog following the existing modal pattern in the codebase. Rather than adding inline styles, we leveraged the established active CSS class pattern already used for authentication and event detail modals.
The payment modal includes:
- A patron name/email lookup field that queries the event's roster
- An amount input field with currency formatting
- A payment method selector (credit card, cash, check, ACH)
- Timestamp capture (automatically set to current UTC time)
- An admin-only toggle visible only when the user holds admin credentials
The event card rendering function was patched to conditionally display a "Log Payment" button for admin users. This button triggers the modal with the current event context pre-populated. The modal reuses the existing apiFetch helper function to POST payment data to a new Lambda handler endpoint.
Backend: Lambda Handlers and DynamoDB Integration
The Lambda function (/tools/shipcaptaincrew/lambda_function.py) required two new handlers inserted before the main lambda_handler routing logic:
handle_payment_log: Accepts POST requests with payment details (event_id, patron_email, amount, method, timestamp). This handler:
- Validates that the requesting user has admin privileges by checking their IAM identity or session token
- Queries the DynamoDB Events table to confirm the event exists and the patron is on the roster
- Writes a new item to a
Paymentstable with a composite key:PK = event_id#patron_email,SK = timestamp - Returns a 200 response with the written payment record
handle_payment_list: Retrieves all payments for a given event, paginated by timestamp. This allows admins to audit payment history before logging a new entry.
Both handlers leverage existing auth helper functions (verify_admin_token, get_user_context) to ensure only authorized users can access payment APIs.
Gmail Integration for Audit Logging
Payment logging events are sent to a dedicated Gmail inbox for compliance and audit purposes. This required storing Gmail service account credentials in Lambda environment variables:
GMAIL_SERVICE_ACCOUNT_EMAIL: The service account email addressGMAIL_SERVICE_ACCOUNT_KEY: The base64-encoded JSON private keyGMAIL_AUDIT_RECIPIENT: The audit inbox email address
A new helper function, send_audit_email_via_gmail, was added to construct and send emails using the Gmail API. Each payment log triggers an email with:
- Event name and date
- Patron name and email
- Amount and payment method
- Admin who logged the payment
- Server-side timestamp
Infrastructure Changes
DynamoDB Schema Extension
A new Payments
- Table Name:
shipcaptaincrew-Payments - Partition Key (PK):
event_id#patron_email(String) - Sort Key (SK):
timestamp(Number, Unix epoch milliseconds) - GSI:
GSI1-PK: patron_email,GSI1-SK: timestamp— allows querying all payments for a patron across events - TTL: Attribute name
expiration_timestampset to 2,555 days (7 years) for long-term audit retention
Lambda Environment Configuration
Environment variables were added to the Lambda function via the AWS Console and persisted in the merged config payload:
aws lambda update-function-configuration \
--function-name shipcaptaincrew-api \
--environment Variables={GMAIL_SERVICE_ACCOUNT_EMAIL=...,GMAIL_SERVICE_ACCOUNT_KEY=...,GMAIL_AUDIT_RECIPIENT=...}
All credentials were encrypted at rest by AWS Lambda's default KMS encryption and are never logged or returned in function responses.
CloudFront and S3 Routing
The dispatch HTML was redeployed to both the production and staging S3 buckets:
- Production bucket:
s3://shipcaptaincrew-dispatch-prod/ - Staging bucket:
s3://shipcaptaincrew-dispatch-staging/
CloudFront cache was invalidated on the shipcaptaincrew distribution (ID redacted) using wildcard patterns to ensure new HTML was served immediately:
aws cloudfront create-invalidation \
--distribution-id [DIST_ID] \
--paths "/*" "/_staging/*"
Route Corrections: Waiver Endpoint Routing
During testing, we identified that waiver requests (/g/{event_id}/waiver) were incorrectly routed to S3 instead of Lambda, causing the dispatch SPA to attempt parsing the request as an event ID. A new CloudFront behavior was added:
- Path Pattern:
/g/*/waiver - Origin: Lambda Function URL (not S3)
- Cache Policy: Managed-CachingDisabled (since Lambda returns dynamic HTML)
Key Decisions
Why DynamoDB instead of RDS? The existing Events table uses DynamoDB with eventual consistency. Payments, while sensitive, don't require transactional ACID guarantees across multiple rows—each payment is an independent log entry. DynamoDB's partition key design (event_id#patron_email) and GSI enable both event-scoped and patron-scoped queries without complex joins.
Why Gmail instead of SES? While SES is already integrated for admin notifications, Gmail's audit inbox provides a familiar, searchable archive accessible to business users. The service account approach avoids storing personal credentials and integrates with Google Workspace's audit logging.
Why a modal instead of a dedicated page? Admins are already in the event details view. Adding a modal within that context reduces context switching and keeps the UX consistent with existing "edit event" and "view roster" modals.