Building a Payment Logging System for Patron Management in a Serverless SPA
What Was Done
Added a complete payment logging pipeline to the Ship Captain Crew (SCC) event management system, enabling admins to record patron payments through a new modal interface. The system integrates AWS Lambda handlers with an HTML5 modal UI, DynamoDB persistence, and email notifications via SES.
Architecture Overview
The SCC tool is a serverless single-page application (SPA) deployed across three layers:
- Frontend: Static dispatch HTML served from S3 bucket
queenofsandiego.com-scc-dispatchthrough CloudFront distribution (CF dist for shipcaptaincrew) - Backend: AWS Lambda function at
/tools/shipcaptaincrew/lambda_function.pyhandling all API routes - Data: DynamoDB table storing events, with payment records embedded in event items
The payment logging feature follows this flow: Admin clicks "Log Payment" → Modal form submits to Lambda → Lambda validates and writes to DynamoDB → Lambda sends SES email notification → Frontend receives confirmation.
Lambda Backend Implementation
Added two new handler functions to lambda_function.py before the main lambda_handler routing logic:
handle_payment_log(event_id, patron_name, amount, payment_method, notes)— Validates inputs, writes payment record to DynamoDB event item, returns success/error responsesend_payment_notification(patron_name, amount, event_id)— Uses AWS SES to email admin confirmation; references environment variables for sender email and recipient list
The handlers integrate with existing Lambda infrastructure:
- Uses existing DynamoDB table schema (payment records added as nested objects in event items)
- Respects admin authentication via existing
verify_admin_token()function - Leverages SES client already initialized for other email features
- Follows existing error response pattern (400 for validation, 500 for service errors)
New API route added to lambda_handler routing:
POST /api/payment/log
Required: event_id, patron_name, amount, payment_method
Optional: notes
Auth: Admin token required
Returns: { "status": "success", "payment_id": "...", "timestamp": "..." }
The handler validates:
- Event exists in DynamoDB
- Amount is positive numeric value
- Payment method is one of: ["cash", "card", "check", "venmo"]
- Patron name is non-empty string
Frontend Modal Interface
Modified /tools/shipcaptaincrew/index.html to add payment logging UI:
- New modal HTML inserted near existing modals (follows existing
modal-activeclass pattern for visibility toggling) - Form inputs for: patron name, amount, payment method dropdown, optional notes field
- Submit button triggers
logPayment()JavaScript function - "Log Payment" button added to event card context menu (visible only for admins)
JavaScript implementation:
async function logPayment(eventId, formData) {
const response = await fetch('/api/payment/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${adminToken}`
},
body: JSON.stringify({
event_id: eventId,
patron_name: formData.name,
amount: parseFloat(formData.amount),
payment_method: formData.method,
notes: formData.notes || null
})
});
if (response.ok) {
const result = await response.json();
showNotification(`Payment logged for ${formData.name}: $${formData.amount}`);
closePaymentModal();
refreshEventCard(eventId);
} else {
showError('Failed to log payment');
}
}
Infrastructure & Deployment
DynamoDB Schema: Payment records stored as array within existing event items:
{
"event_id": "2026-05-23-corporate-event",
"event_name": "Corporate Mixer",
"payments": [
{
"payment_id": "pay_1234567890",
"patron_name": "John Smith",
"amount": 150.00,
"payment_method": "card",
"notes": "CC ending in 4242",
"logged_by": "admin@queenofsandiego.com",
"timestamp": "2026-01-15T14:32:00Z"
}
]
}
Lambda Environment Variables: No new env vars required; uses existing:
SES_SENDER_EMAIL— Verified SES sender for notificationsADMIN_NOTIFY_EMAILS— Comma-separated list of admin emails (updated during deployment)
Deployment Process:
- Update
lambda_function.pywith new handlers - Create deployment zip:
zip -r lambda.zip lambda_function.py - Deploy to Lambda:
aws lambda update-function-code --function-name shipcaptaincrew --zip-file fileb://lambda.zip - Update dispatch HTML in S3:
aws s3 cp index.html s3://queenofsandiego.com-scc-dispatch/index.html - Invalidate CloudFront cache:
aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/*"
Staging Validation: Before production deployment, changes are pushed to staging slot:
- HTML deployed to
s3://queenofsandiego.com-scc-dispatch/_staging/index.html - CloudFront behavior routes
/_staging/*to staging bucket - Admin tests payment logging at staging URL before production cutover
Key Technical Decisions
Payment Records as Nested Arrays: Rather than creating a separate DynamoDB table for payments, we embed payment records within event items. This keeps related data together, simplifies queries, and avoids additional partition key complexity.
SES for Notifications: Email notifications use existing SES integration rather than introducing a new messaging service. This reduces infrastructure surface area and leverages already-configured sender identity.
Admin-Only Access: Payment logging requires existing admin token authentication. No new auth mechanism was introduced; we reuse the established token verification pattern.
Modal UX Pattern: Payment modal follows existing UI conventions in the codebase (class-based visibility toggling, consistent form styling, standard error/success feedback).