Adding Payment Logging to the Ship Captain Crew Event Management Tool
This session focused on implementing a payment logging feature for the Ship Captain Crew (SCC) event management system. The work involved coordinating changes across a Lambda function, DynamoDB schema validation, a CloudFront distribution, and a single-page application (SPA) dispatch interface. The goal: enable crew admins to record patron payments directly from the event dashboard, with full audit trail support.
What Was Done
- Added Gmail credential storage and validation to the Lambda environment configuration
- Implemented payment handler logic in the Lambda function at
/tools/shipcaptaincrew/lambda_function.py - Created a "Log Payment" modal UI in the dispatch SPA (
/tools/shipcaptaincrew/index.html) - Updated CloudFront behavior routing to correctly dispatch waiver requests to Lambda
- Diagnosed and documented a slug naming convention violation in event IDs
- Deployed both Lambda code and HTML to staging for integration testing
Technical Details: Lambda Payment Handler
The Lambda function required two new pieces: Gmail helper functions and a payment-logging handler. The Gmail helpers authenticate using service account credentials stored in environment variables and construct SES-compatible message payloads.
Handler Registration: A new route was added to the Lambda routing logic (near line 1500 in lambda_function.py) to intercept POST requests to /api/events/{event_id}/payment. This handler:
- Validates the incoming request includes required fields:
patron_name,amount_paid,payment_method, andtimestamp - Queries the DynamoDB table (schema confirmed via
describe-table) to verify the event exists and is mutable - Appends a payment record to the event's
paymentsarray attribute (if not present, initializes it) - Updates a running
total_receivedfield to track aggregate payments per event - Logs the transaction with the admin's authenticated identity for audit purposes
- Returns a 200 response with the updated payment list
Why This Approach: Rather than creating a separate payments table, we store payments as a nested array within each event item. This keeps related data co-located, reduces DynamoDB read costs (one GetItem instead of Query), and simplifies transactional consistency—a single UpdateItem operation is atomic. The cost is slightly larger event items, but for typical event sizes (5–50 payments), this is negligible.
Frontend: Dispatch HTML Modal Integration
The dispatch SPA required a new modal component to capture payment details. Rather than adding new HTML templates, we followed the existing modal pattern already used for event creation and editing.
Modal Structure: The new "Log Payment" modal (inserted in index.html around line 1200) includes:
- Patron name input field
- Amount paid numeric input with currency symbol
- Payment method dropdown (Cash, Check, Card, Other)
- Optional notes textarea
- Submit button that calls the new
submitPayment()function
State Management: The modal visibility is controlled via CSS class toggling (the existing active class pattern seen in the event-edit modal). The submitPayment() function:
// Pseudo-code for clarity
async function submitPayment(eventId) {
const payload = {
patron_name: document.getElementById('payment-patron').value,
amount_paid: parseFloat(document.getElementById('payment-amount').value),
payment_method: document.getElementById('payment-method').value,
notes: document.getElementById('payment-notes').value,
timestamp: new Date().toISOString()
};
const response = await apiFetch(`/api/events/${eventId}/payment`, {
method: 'POST',
body: JSON.stringify(payload)
});
// Refresh event display if successful
if (response.ok) {
await loadEventDetails(eventId);
hidePaymentModal();
}
}
The modal is triggered by a new "Log Payment" button added to the event card footer, visible only when the viewing user has admin credentials (enforced via the existing isAdmin() check).
Infrastructure: CloudFront and Environment Variables
CloudFront Behavior Fix: During testing, waiver requests at paths like /g/2026-05-23/waiver were incorrectly routing to S3 instead of Lambda. The S3 fallback would return HTML, which the SPA tried to parse as JSON, causing a "Could not load event" error. The fix involved adding a new CloudFront behavior:
Path Pattern: /g/*/waiver
Origin: Lambda Function URL (not S3)
Allowed Methods: GET, POST
TTL: 0 (no caching for this dynamic route)
Why This Matters: CloudFront evaluates behaviors in order of specificity. The catch-all S3 behavior was matching before the Lambda behavior could claim these requests. By adding a more specific /g/*/waiver pattern with Lambda as the origin, we ensure waiver requests reach the correct handler.
Environment Variables: The payment feature requires Gmail credentials for sending confirmation emails. These were added to the Lambda environment via the AWS Lambda console (or CLI). The deployment process merged existing vars (ADMIN_PASS_HASH, API_BASE_URL, etc.) with new Gmail-related vars and pushed the combined set to Lambda via:
aws lambda update-function-configuration \
--function-name shipcaptaincrew-handler \
--environment Variables={...merged-vars-json...}
The environment update was allowed to settle (typically 5–10 seconds) before deploying new code, ensuring no race condition between variable availability and code execution.
Deployment Process
Staging-First Approach: Both the Lambda code and HTML were deployed to staging URLs before production. The Lambda deployment involved:
- Zipping the updated
lambda_function.pyand dependencies - Uploading to the shipcaptaincrew S3 bucket
- Publishing a new Lambda version via
update-function-code - Waiting for code deployment to complete
The HTML dispatch file was deployed to an S3 staging slot and served via a CloudFront staging distribution. CloudFront cache was invalidated using:
aws cloudfront create-invalidation \
--distribution-id STAGING_DIST_ID \
--paths "/_staging/*"
This ensured the browser received the fresh HTML immediately, not a cached version.
Diagnosed Issues (Not Yet Fixed)
While implementing the payment feature, a naming convention violation was discovered: event slug 2026-05-23 does not follow the documented format of YYYY-MM-DD-firstname[-period]. This slug lacks a captain's name, making it ambiguous in list contexts. A follow-up task was added to audit and correct existing slugs.
What's Next
The payment logging