```html

Building a Private Payment Intake Pipeline for Charter Operations: The JADA Deposit Reporting System

Charter yacht operations live on a razor's edge between transparency and privacy. Crew needs to know if a booking is confirmed; clients need confirmation their deposit arrived; the operator needs an audit trail. But financial details—amounts, payment methods, sender information—must never leak to crew. This post details how we extended JADA's automation stack to handle Zelle deposit reporting with strict privacy boundaries and zero manual steps.

The Problem Statement

C.B. receives Zelle transfers from clients on her personal phone. When a $500 deposit lands for a booking like 2026-06-14-sarah-sunset, a cascade of events should fire automatically:

  • A private accounting record (CB only, never crew-visible)
  • A branded HTML confirmation email to the client
  • A calendar event status update in JADA Internal
  • The crew dispatch page flips from "tentative" to "confirmed"
  • The guest page shows confirmation state

The challenge: C.B. needs a phone-friendly intake method that doesn't require SSH, doesn't expose her Zelle screenshots to crew, and integrates with existing infrastructure. She already has a daemon polling her CB notes; we extended that.

Architecture: The Three-Layer Approach

Layer 1: Private Receipt Storage (S3 + Pre-signed URLs)

Zelle screenshots are accounting artifacts, not automation triggers. They go into a private S3 bucket with no public ACL and no CloudFront distribution.

  • Bucket: jada-private-receipts (us-west-2)
  • Prefix structure: jada-private-receipts/deposits/YYYY/MM/DD/booking-id-zelle-timestamp.png
  • Bucket policy: Deny all public access, allow only EC2 instance role (jada-daemon-role) and CB's IAM user (cb-ops)
  • No CloudFront distribution — crew has no path to this bucket
  • Server-side encryption: AES-256 default
  • Versioning: Enabled (audit trail)

When a deposit is reported, C.B. uploads a screenshot via a pre-signed URL (valid 15 minutes). The screenshot is filed away; the automation doesn't wait for it or depend on it.

Layer 2: Intake Signal (Extended CB Notes Daemon)

The existing handle_cb_notes.py daemon at 34.239.233.28 polls CB's notes every 60 seconds. We extended it to recognize a structured deposit report:

DEPOSIT RECEIVED - 2026-06-14-sarah-sunset - Zelle $500

This is human-readable, parseable, and fits the existing note-to-automation pipeline. No new infrastructure; no new auth. The daemon:

  • Polls CB notes (existing behavior)
  • Pattern-matches on DEPOSIT RECEIVED - (\S+) - (\S+) (\$[\d.]+)
  • Extracts: booking ID, payment method, amount
  • Invokes process_deposit_received() Lambda function
  • Replies to CB: "✓ Deposit logged for sarah-sunset, confirmation queued"

Why this approach? Zero new tools for C.B. to learn. She already uses CB notes to trigger other automations (crew assignments, schedule changes). This feels native.

Layer 3: State Machine & Downstream Events

Once the daemon signals a deposit, a Lambda function (process_deposit_received) orchestrates the state change:


# Pseudocode flow in process_deposit_received():
1. Query DynamoDB: bookings table, filter by booking_id
2. Validate booking exists, not already paid
3. Create audit log entry in CloudWatch Logs (private, CB-readable only)
4. Update DynamoDB: bookings.deposit_status = "received", deposit_amount, deposit_date
5. Trigger SNS topic: "jada-deposit-received"
6. Publish to EventBridge: booking state change event

That SNS topic fans out to three subscribers:

  • Lambda: send-client-confirmation-email — Generates branded HTML from template, sends via SES
  • Lambda: update-gcal-event — Patches the JADA Internal calendar event with "Deposit Received" marker
  • DynamoDB Stream → Lambda: update-crew-dispatch — Flips the booking card status, crew page updates in real-time

The Crew Visibility Boundary

The crew dispatch page (crew.queenofsandiego.com/bookings) operates on three states, but shows no financial data in any state:

  • "Tentative" — No deposit logged. Red banner: "Do not depart until deposit received and full payment logged."
  • "Confirmed" — Deposit received. Red banner still active: "Full payment must be logged before departure authorization."
  • "Cleared for Departure" — Full payment logged. Green. Crew may depart.

The DynamoDB schema separates concerns:


bookings table:
  PK: booking_id (2026-06-14-sarah-sunset)
  crew_visible_state: ["tentative", "confirmed", "cleared"]
  deposit_received: boolean (crew sees this as a state enum, not a fact)
  deposit_amount: number (CB-only attribute, not projected to crew)
  full_payment_received: boolean (crew sees this as state, not amount)
  payment_amount_total: number (CB-only attribute)

The crew-facing API at crew.queenofsandiego.com/api/bookings uses a projection that strips all payment amounts and only returns the state enum.

Full Payment Intake (Second Event Type)

The same daemon handles full payment:

FULL PAYMENT RECEIVED - 2026-06-14-sarah-sunset - Zelle $2000

This triggers process_full_payment_received(), which:

  • Updates bookings.full_payment_received = true
  • Sets crew_visible_state = "cleared"
  • Triggers a green confirmation to crew and client
  • Updates the guest page confirmation state

Guest Page Integration

The guest page at /g/YYYY-MM-DD-firstname (e.g., /g/2026-06-14-sarah) already displays confirmation state. With this change:

  • Page listens to the same EventBridge stream
  • When crew_visible_state changes, the page re-renders in near real-time
  • Client sees