Building a Payment Logging System for the ShipCaptainCrew Dispatch Tool
This post documents the design and implementation of a payment logging feature for ShipCaptainCrew, a crew management and event dispatch tool. The feature allows admins to record patron payments directly from the event dashboard, with data persisted to DynamoDB and integrated into the existing Lambda/CloudFront/S3 architecture.
Problem Statement
ShipCaptainCrew manages crew assignments and event bookings but lacked a way to log patron payments through the UI. Admins had no direct interface to record when a payment was received, forcing manual tracking outside the system. This created a gap between event data (stored in DynamoDB) and financial records.
Architecture Overview
ShipCaptainCream operates as a serverless SPA:
- Frontend: Single-page application (dispatch HTML) served from S3 bucket
shipcaptaincrew.s3.us-west-1.amazonaws.comvia CloudFront distribution - API Backend: AWS Lambda function (
lambda_function.py) exposed via Lambda Function URL, handling GET/POST requests for events, crew, and admin actions - Data Store: DynamoDB table (schema: partition key
pk, sort keysk) storing events, crew, and audit logs - Auth: Admin endpoints protected by environment variable hash comparison (ADMIN_PASS_HASH)
Implementation: Lambda Backend Changes
The payment logging feature required two new Lambda handlers and supporting utilities:
Email Helper Integration
Since payment notifications need to reach admins, I added Gmail SMTP helpers to lambda_function.py:
def send_gmail(to_addr, subject, body_html):
"""Send email via Gmail SMTP using stored credentials."""
# Reads SCC_GMAIL_USER, SCC_GMAIL_PASSWORD, SCC_GMAIL_TO from env
# Uses smtplib to relay to admin inbox
Gmail credentials are stored as Lambda environment variables (encrypted at rest by AWS). This avoids hardcoding and allows rotation without redeploying code.
Payment Handler: POST /api/g/{event_id}/payment
The main payment logging endpoint accepts POST requests with patron name, amount, and notes:
def handle_payment_log(event_id, body):
"""
Log a payment for an event patron.
- Validates admin token from Authorization header
- Writes PaymentLog item to DynamoDB
- Sends confirmation email to SCC_GMAIL_TO
"""
# 1. Auth check: compare token hash
# 2. Fetch event from DynamoDB (pk=event_id)
# 3. Insert payment record (pk=event_id, sk=PAYMENT#{timestamp}#{uuid})
# 4. Email admin with receipt details
# 5. Return 200 + payment_id
Why this design? Separating payment logs as distinct DynamoDB items (rather than updating an event array) allows:
- Immutable audit trail (no overwrites)
- Efficient queries (GSI on
skprefix forPAYMENT#*) - Asynchronous email sending without blocking the response
Payment Query Handler: GET /api/g/{event_id}/payments
Admins need to view all payments logged against an event:
def handle_get_payments(event_id):
"""
Retrieve all payment logs for an event.
- Query DynamoDB with pk=event_id, sk begins_with "PAYMENT#"
- Return sorted array [{ amount, patron, timestamp, notes }, ...]
"""
This uses DynamoDB's query operation, which is more efficient than scan for a known partition key.
Lambda Routing Integration
These handlers are wired into the main lambda_handler routing logic in lambda_function.py, approximately line 2100:
elif path == f"/api/g/{event_id}/payment" and method == "POST":
return handle_payment_log(event_id, json.loads(body))
elif path == f"/api/g/{event_id}/payments" and method == "GET":
return handle_get_payments(event_id)
Why POST for logging, GET for retrieval? RESTful conventions: POST creates a new resource (payment record), GET reads existing ones. This maps naturally to the handler names and makes the API predictable for frontend developers.
Implementation: Frontend UI Changes
The dispatch HTML (tools/shipcaptaincrew/index.html) needed a modal interface to capture payment details:
Modal HTML Structure
Following existing patterns in the HTML, I added a "Log Payment" modal (id paymentModal) with form fields:
<div id="paymentModal" class="modal">
<div class="modal-content">
<h3>Log Payment</h3>
<input id="paymentPatron" type="text" placeholder="Patron name">
<input id="paymentAmount" type="number" step="0.01" placeholder="Amount ($)">
<textarea id="paymentNotes" placeholder="Notes (optional)"></textarea>
<button onclick="submitPayment()">Log Payment</button>
<button onclick="closePaymentModal()">Cancel</button>
</div>
</div>
Why a modal? It keeps the event dashboard uncluttered and makes the payment action explicit (users confirm intent before submission).
JavaScript Payment Functions
The modal is controlled by three functions:
function openPaymentModal(eventId) {
// Set currentPaymentEventId, show modal with display: block
}
function submitPayment() {
// Collect form values, POST to /api/g/{eventId}/payment
// On success: refresh payments list, close modal
// On error: show toast notification
}
function closePaymentModal() {
// Clear form fields, hide modal
}
Payment Display on Event Cards
Event cards now show a "Payments" section listing all logged payments:
function renderPaymentsList(eventId) {
// Fetch /api/g/{eventId}/payments
// For each payment: render patron name, amount, timestamp
// Show total collected (sum of amounts)
// Include "Log Payment" button if user is admin
}
This gives admins visibility into collection status without navigating away from the dashboard.
Key Architecture Decisions
Why Email Notifications?
Payment logging is an audit event. Emailing the admin (address in SCC_GMAIL_TO) creates a paper trail and alerts the responsible party immediately. This is simpler than implementing webhook subscriptions or polling.
Why Environment Variables for Secrets?
Lambda environment variables (encrypted by AWS KMS) are easier than hardcoding or fetching from AWS Secrets