Deploying a Receipt Upload Portal and Automating Port Sheet Generation with Google Sheets + Lambda
This session involved two parallel infrastructure projects: standing up a receipt management system for a trailer rental business, and fully automating the monthly port sheet workflow that generates charter revenue reports. Both required coordinating static site hosting, serverless execution, OAuth token management, and email delivery.
Project 1: Receipt Portal Deployment to quickdumpnow.com/books
The trailer rental business needed a receipt upload destination at https://quickdumpnow.com/books. The site structure already existed locally but hadn't been deployed to production.
Static Site Structure
- Local path:
/Users/cb/Documents/repos/sites/quickdumpnow.com/books/index.html - S3 bucket:
quickdumpnow.com(CloudFront-backed) - CloudFront distribution: points to the S3 bucket origin
- Route53:
quickdumpnow.comALIAS record points to CloudFront distribution
Deployment Process
The challenge was that CloudFront had a custom error response rule redirecting all 404s to the homepage. This made sense for SPA routing but broke attempts to access /books as a directory-style path.
Solution: Upload the books page under two S3 keys to handle both URL patterns:
# Upload to explicit path for /books/ requests
aws s3 cp books/index.html s3://quickdumpnow.com/books/index.html
# Upload to bare /books key for pretty URL handling
aws s3 cp books/index.html s3://quickdumpnow.com/books
Then invalidate CloudFront to clear edge cache:
aws cloudfront create-invalidation \
--distribution-id [DIST_ID] \
--paths "/books" "/books/*" "/robots.txt"
robots.txt Update
Updated /Users/cb/Documents/repos/sites/quickdumpnow.com/robots.txt to block indexing of the receipts area, keeping it private to the business:
User-agent: *
Disallow: /books
This prevents search engines from discovering or caching receipt metadata while keeping the site publicly crawlable otherwise.
Project 2: Automated Port Sheet Generation Pipeline
The port sheet workflow automates monthly charter revenue reporting. Previously this was manual; now it's fully serverless with OAuth-based Google Sheets integration and Lambda execution.
Architecture Overview
- Data source: JADA Port Log 2026 spreadsheet (Google Sheets)
- Orchestration:
/Users/cb/Documents/repos/tools/jada_port_sheet.py(Python, runs locally or in Lambda) - Authentication: OAuth 2.0 refresh token stored in Lambda environment variables
- New auth tool:
/Users/cb/Documents/repos/tools/reauth_jada_calendar.py(handles token refresh) - Apps Script:
/Users/cb/Documents/repos/sites/queenofsandiego.com/PortSheetReporter.gs(Google Apps Script bound to the sheet) - Delivery: Email via Gmail API to Maria, Edwin, and Carolina
Google Sheets Schema
The JADA Port Log 2026 spreadsheet has multiple tabs:
- Template tab: Row formats and column structure (Month, Date, Vessel, Captain, etc.)
- Monthly tabs: One per month (March 2026, April 2026, etc.) containing actual entries
- Data format: Specific column positions with row heights and formatting preserved via openpyxl
The Python script reads from Google Sheets API using the spreadsheet ID, extracts all rows from the current month tab, and formats them for email delivery.
OAuth Token Management Challenge
The original calendar credentials had limited scope. Initial attempts to refresh the token failed because:
- Problem: The Drive API client had Google Cloud console credentials (service account-like), but calendar API required user OAuth
- Root cause: Two different OAuth applications with different credentials
- Solution: Created
reauth_jada_calendar.pyto perform full OAuth flow on port 8765, obtaining a fresh refresh token with proper scopes
The reauth script:
# Binds to local port 8765 for OAuth callback
# Opens browser for user consent
# Extracts authorization code from callback
# Exchanges code for refresh + access tokens
# Stores GCAL_REFRESH_TOKEN for Lambda environment
Key decision: Store only the refresh token in Lambda, never the full credentials file. The script regenerates access tokens on each port sheet run.
Port Sheet Data Entry
Added a charter entry for Joseph Zurek with payout $1845.72:
- Sheet ID: Found the correct
sheetIdinteger for the April tab via API introspection - Row format: Date, Vessel Name, Captain Name, Payout Amount
- Preservation: Used openpyxl to maintain row heights, column widths, and formatting from the template
- Append method: Google Sheets API
append_valueswith raw values, allowing the sheet's formatting rules to apply
Lambda Deployment
Updated the Lambda function with new environment variable:
GCAL_REFRESH_TOKEN=[refreshed_token_value]
This allows the port sheet script to obtain short-lived access tokens without hardcoding credentials:
def get_calendar_access_token(refresh_token):
"""Exchange refresh token for access token"""
# POST to https://oauth2.googleapis.com/token
# Returns access_token valid for 1 hour
Email Delivery
The port sheet script reads all entries from the current month tab, formats them into a markdown/HTML email, and sends via Gmail API to:
- Maria (operations lead)
- Edwin (accounting)
- Carolina (admin)
Email includes:
- Month and year
- Formatted charter table (Vessel, Captain, Payout)
- Total revenue sum
- Links to the full port log sheet for live data
Key Decisions
- Dual S3 key upload: Handles both
/booksand/books/URL patterns without CloudFront rewrites - Refresh token only in Lambda: Reduces secret surface area and allows token rotation