Building a Self-Contained Payment Logging System for Multi-Tenant Property Management

What Was Done

We built a complete payment ingestion pipeline for a property management tenant portal, isolated entirely within the dangerouscentaur.com domain. The system allows property managers to forward bank payment notifications (Zelle transfers, etc.) to an email alias, which automatically parses the message, extracts payment details, and logs them to a tenant receipts database—all without manual data entry.

Key accomplishments:

  • Generated and deployed fresh tenant credentials to a secure hub at https://3028fiftyfirststreet.92105.dangerouscentaur.com/
  • Sent credential emails via SES using a properly-configured dangerouscentaur.com domain alias
  • Wired an email-to-payment-log pipeline using AWS Lambda and Google Apps Script
  • Created an admin token system for Lambda endpoint authentication
  • Isolated all infrastructure from the main queenofsandiego.com domain

Technical Details

Credential Management & Deployment

Tenant credentials were generated fresh and embedded in the hub's HTML at /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html. The hub pulls these credentials from a simple data structure and serves them to tenants who complete identity verification. After updating the HTML, we invalidated the CloudFront distribution cache:

aws cloudfront create-invalidation \
  --distribution-id [DIST_ID] \
  --paths "/*"

Credentials were then sent via AWS SES from a verified sender email within the dangerouscentaur.com domain. This required proper SPF/DKIM setup for the domain to avoid bounce/spam issues.

Email-to-Payment Lambda Pipeline

Two Lambda functions form the backbone of payment ingestion:

  • lambda-email-parser: Receives raw emails from SES (via SNS trigger), parses the message body for payment amount/sender/date, and invokes the receipt-action Lambda
  • lambda-receipt-action: Accepts structured payment records and writes them to receipts.json in S3

The email parser lives at /scripts/lambda-email-parser/lambda_function.py and uses regex patterns to extract Zelle payment details from common bank email formats. When a property manager forwards a payment confirmation email to the alias (configured in ImprovMX), it triggers the SNS-to-Lambda pipeline.

# Example parser logic (pseudocode)
def parse_zelle_email(body):
    # Extract amount: looks for "$X,XXX.XX" patterns
    amount_match = re.search(r'\$(\d+,?\d+\.\d{2})', body)
    # Extract date: looks for "Date: MM/DD/YYYY"
    date_match = re.search(r'Date:\s+(\d{1,2}/\d{1,2}/\d{4})', body)
    return {
        'amount': amount_match.group(1),
        'date': date_match.group(1),
        'method': 'zelle'
    }

The receipt-action Lambda at /scripts/lambda-receipt-action/lambda_function.py was updated with a new log_payment admin action. This action validates an admin token (stored as an environment variable), reads the current receipts.json, appends the new payment record, and writes it back:

def handle_log_payment(event, admin_token_env):
    # Validate admin token from request headers
    provided_token = event.get('headers', {}).get('X-Admin-Token')
    if provided_token != admin_token_env:
        return {'statusCode': 403, 'body': 'Unauthorized'}
    
    # Read current receipts
    receipts = s3_get_json('bucket-name', 'receipts.json')
    
    # Append new payment
    receipts.append({
        'tenant_id': event['body']['tenant_id'],
        'amount': event['body']['amount'],
        'date': event['body']['date'],
        'method': event['body']['method'],
        'logged_at': datetime.utcnow().isoformat()
    })
    
    # Write back
    s3_put_json('bucket-name', 'receipts.json', receipts)

Google Apps Script Integration

The WarmLeadResponder.gs script was extended to recognize forwarded payment emails and trigger the Lambda. A new handler in the command routing section watches for emails matching the pattern "Zelle payment" or similar indicators:

// In the command execution block
if (subject.includes('Zelle') || subject.includes('payment')) {
  var paymentData = parsePaymentFromEmail(messageBody);
  var lambdaUrl = PropertiesService.getScriptProperties()
    .getProperty('RECEIPT_ACTION_LAMBDA_URL');
  var adminToken = PropertiesService.getScriptProperties()
    .getProperty('ADMIN_TOKEN');
  
  var response = UrlFetchApp.fetch(lambdaUrl, {
    method: 'post',
    headers: {
      'X-Admin-Token': adminToken,
      'Content-Type': 'application/json'
    },
    payload: JSON.stringify({
      action: 'log_payment',
      body: paymentData
    })
  });
}

Infrastructure & Configuration

  • S3 Bucket: dangerouscentaur-tenant-data stores receipts.json and hub assets
  • CloudFront Distribution: Serves the tenant hub with caching headers optimized for credential changes
  • Lambda Functions: Both deployed with public URLs enabled and resource-based policies allowing SNS invocation
  • SES Verified Sender: noreply@dangerouscentaur.com (or similar), configured with DKIM tokens added to Route53
  • ImprovMX Alias: Email forwarder configured to trigger SNS topic on payment email receipt
  • Environment Variables: ADMIN_TOKEN set on receipt-action Lambda for authentication

Key Decisions

Why isolated from queenofsandiego.com? The tenant portal needed complete operational independence. Any downtime, security issue, or credential compromise in the main domain should not affect the tenant-facing system. Using a dedicated subdomain and separate AWS resources enforces this boundary.

Why email-based payment logging? Property managers already receive payment confirmations in email. Forwarding to an alias is friction-free—no new tools to learn. The regex-based parser handles the messiness of bank emails gracefully and fails safely (logging unparseable messages for manual review).

Why admin token authentication? The Lambda endpoint is internet-facing. An admin token (stored securely in environment variables, not in code) prevents unauthorized payment logging while keeping the endpoint simple and stateless.

Why Apps Script as the orchestrator? G