Flask backend for Kwartzlab's reimbursement request and purchase approval forms. Handles form submissions, file uploads to Google Drive, writes to Google Sheets, and sends notifications via email and Slack.
- Two form types: Reimbursement Request and Purchase Approval
- File upload to Google Drive with organization-restricted permissions
- Automatic ID generation with race condition handling
- Email notifications (dual emails: list notification + submitter acknowledgment)
- Slack notifications for Purchase Approvals
- hCaptcha verification
- Rate limiting (production only)
- Input validation and sanitization
- Python 3.8+
- Google Cloud Project with:
- Google Sheets API enabled
- Google Drive API enabled
- Service Account created with credentials
- Google Workspace account with:
- Shared Drive access (service accounts have no storage quota)
- Google Sheets for storing submissions
- Folders in Shared Drive for file storage
- Slack workspace (for Purchase Approval notifications)
- Gmail or SMTP server (for email notifications)
- hCaptcha account
# Clone the repository
git clone <repository-url>
cd backend
# Create virtual environment
python -m venv venv
# Activate virtual environment
# On macOS/Linux:
source venv/bin/activate
# On Windows:
venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt- Go to Google Cloud Console
- Create a new project or select existing one
- Enable Google Sheets API and Google Drive API
- Create a Service Account:
- Go to IAM & Admin → Service Accounts
- Create Service Account
- Download JSON credentials
- Save the credentials file as
credentials.jsonin the project root - Add the service account email to your Google Shared Drive:
- Open your Shared Drive in Google Drive
- Right-click → Manage members
- Add the service account email with "Content Manager" or "Manager" role
- The service account will automatically have access to sheets and folders within the Shared Drive
Create two Google Sheets in your Shared Drive:
Reimbursement Request Sheet - Columns:
- ID | Timestamp | First Name | Last Name | Email | Approval/Project | Vendor | Description | Amount | HST | File Links | Comments
Purchase Approval Sheet - Columns:
- ID | Timestamp | First Name | Last Name | Email | Vendor | Description | Amount | File Links | Comments
Note the Sheet names and IDs from the URLs.
Create two folders in your Shared Drive:
- Reimbursement Request Files
- Purchase Approval Files
Note the folder IDs from the URLs (the long string after /folders/ in the browser).
Create a .env file in the project root:
# Flask Configuration
FLASK_ENV=development
# hCaptcha
CAPTCHA_SECRET=your_hcaptcha_secret_key
# Email Configuration
# Production email addresses (used when FLASK_ENV != development)
OUTBOUND_EMAIL_ADDRESS=treasurer@kwartzlab.ca
RR_RECIPIENT_EMAIL=reimbursements@kwartzlab.ca
PA_RECIPIENT_EMAIL=purchase-approvals@kwartzlab.ca
# Development email addresses (used when FLASK_ENV = development)
DEV_OUTBOUND_EMAIL_ADDRESS=your_dev_email@gmail.com
DEV_RECIPIENT_EMAIL=your_dev_email@gmail.com
# Email server settings
EMAIL_PASSWORD=your_app_specific_password
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
# Google Sheets
RR_SHEET_NAME=Reimbursement Requests
PA_SHEET_NAME=Purchase Approvals
# Google Drive Folders
RR_GOOGLE_DRIVE_FOLDER_ID=your_rr_folder_id
PA_GOOGLE_DRIVE_FOLDER_ID=your_pa_folder_id
# Organization Domain (for file permissions)
ORGANIZATION_DOMAIN=kwartzlab.ca
# Slack (optional, only for Purchase Approvals)
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
# Google Credentials (leave empty for local dev, uses credentials.json)
# GOOGLE_SHEETS_CREDENTIALS=Important Notes:
- For Gmail, use an App Password, not your regular password
credentials.jsonis used for local development (don't commit this file!)- In production, credentials will be in
GOOGLE_SHEETS_CREDENTIALSenvironment variable - Set
FLASK_ENV=developmentto:- Disable rate limiting during development
- Use DEV_OUTBOUND_EMAIL_ADDRESS and DEV_RECIPIENT_EMAIL (all emails go to your dev address)
- In production, omit
FLASK_ENVor set toproductionto:- Enable rate limiting
- Use OUTBOUND_EMAIL_ADDRESS and separate recipient lists (RR_RECIPIENT_EMAIL, PA_RECIPIENT_EMAIL)
# Make sure virtual environment is activated
python app.py
# Server will start on http://localhost:5000Health Check:
curl http://localhost:5000/healthSubmit Reimbursement Request:
curl -X POST http://localhost:5000/submit \
-F "firstName=John" \
-F "lastName=Doe" \
-F "email=john@example.com" \
-F "comments=Test submission" \
-F "expenses=[{\"id\":1,\"approval\":\"Test\",\"vendor\":\"TestCo\",\"description\":\"Test item\",\"amount\":\"10.00\",\"hst\":\"HST included in amount\"}]" \
-F "captchaToken=test_token" \
-F "file0=@/path/to/test.pdf"Submit Purchase Approval:
curl -X POST http://localhost:5000/submit-PA \
-F "firstName=Jane" \
-F "lastName=Smith" \
-F "email=jane@example.com" \
-F "comments=Test PA" \
-F "expenses=[{\"id\":1,\"vendor\":\"TestCo\",\"description\":\"Test item\",\"amount\":\"50.00\"}]" \
-F "captchaToken=test_token" \
-F "file0=@/path/to/test.pdf"backend/
├── app.py # Main Flask application
├── config.py # Configuration from environment variables
├── requirements.txt # Python dependencies
├── credentials.json # Google service account (local only, gitignored)
├── .env # Environment variables (gitignored)
├── services/ # Service modules
│ ├── __init__.py
│ ├── google_auth.py # Google API authentication
│ ├── google_sheets.py # Google Sheets operations
│ ├── google_drive.py # Google Drive file uploads
│ ├── notifications.py # Email and Slack notifications
│ ├── validation.py # Input validation and sanitization
│ └── utils.py # Utility functions (profiling decorator)
└── templates/ # Email HTML template
└── email_template.html # Unified template for all emails
The system generates sequential IDs. To prevent duplicate IDs when multiple submissions occur simultaneously:
- Generate next ID based on last entry
- Upload files
- Check if ID is still unused (right before writing)
- If duplicate detected, clean up files and retry with exponential backoff (max 5 attempts)
- Files uploaded to Google Drive Shared Drive (service accounts have no storage)
- Each submission gets its own subfolder (named by submission ID)
- Files restricted to organization domain members
- Links (not attachments) sent in emails to avoid size limits
Two emails sent per submission:
- List notification - Full details to the appropriate mailing list:
- Reimbursement Requests → RR_RECIPIENT_EMAIL
- Purchase Approvals → PA_RECIPIENT_EMAIL
- Acknowledgment - Thank you email to submitter with submission copy
In development mode (FLASK_ENV=development), all emails are sent to DEV_RECIPIENT_EMAIL instead of production mailing lists.
Backend validates and sanitizes all inputs:
- File types (PDF, images, spreadsheets, documents)
- File sizes (10MB per file, 50MB total)
- Text field lengths
- Email format
- Amount values (positive numbers, max $1M)
- HTML tag removal from all text inputs
- Disabled in development (
FLASK_ENV=development) - Enabled in production (10 submissions/hour per IP)
- Uses
X-Forwarded-Forheader from Cloud Run proxy
The @log_execution_time decorator is available in services/utils.py and can be applied to any function to measure execution time:
from services.utils import log_execution_time
@log_execution_time
def your_function():
# Function execution time will be printed to console
passThis decorator is already applied to key functions in app.py, notifications.py, and other service modules.
from services.utils import log_execution_time
@log_execution_time
def your_function():
# Function execution time will be printed to console
passIn development, you can temporarily bypass captcha by modifying verify_hcaptcha() in app.py:
def verify_hcaptcha(token):
if Config.FLASK_ENV == 'development':
return True # Skip verification in dev
# ... rest of functionThe sheets client is cached globally. To clear it during development, restart the Flask server.
- Never commit
credentials.jsonor.envfiles .gitignoreis configured to exclude sensitive files- All user input is sanitized before storage
- Files are validated for type and size on backend (don't trust frontend)
- Service account has minimum required permissions
"Missing required environment variables"
- Check that
.envfile exists and contains all required variables - Ensure
.envis in the same directory asapp.py - In development, verify you have DEV_OUTBOUND_EMAIL_ADDRESS and DEV_RECIPIENT_EMAIL set
- In production, verify you have OUTBOUND_EMAIL_ADDRESS, RR_RECIPIENT_EMAIL, and PA_RECIPIENT_EMAIL set
"Error accessing Google Sheet"
- Verify service account email has access to the sheets
- Check sheet names match exactly (case-sensitive)
- Ensure Google Sheets API is enabled in your project
"Error uploading to Google Drive"
- Verify service account has access to the Shared Drive folders
- Check folder IDs are correct
- Ensure Google Drive API is enabled
"Email failed to send"
- For Gmail, use App Password, not regular password
- Check SMTP settings are correct
- In development, verify DEV_OUTBOUND_EMAIL_ADDRESS and EMAIL_PASSWORD are correct
- In production, verify OUTBOUND_EMAIL_ADDRESS and EMAIL_PASSWORD are correct
- Ensure the sender email has appropriate permissions
Rate limiting during development
- Set
FLASK_ENV=developmentin.envto disable rate limiting
Emails going to wrong recipients in development
- Verify
FLASK_ENV=developmentis set in.env - Check that DEV_RECIPIENT_EMAIL is configured correctly
- All emails (both list and acknowledgment) will go to DEV_RECIPIENT_EMAIL in development mode
After local development is working:
- Test all integrations end-to-end
- Verify email templates render correctly
- Test with frontend application
- Prepare for deployment (see deployment section - to be added)