═══════════════════════════════════════════════════════════════════════════════════
PHASE 1 — REQUEST CREATION
═══════════════════════════════════════════════════════════════════════════════════
[ Client / Media Buyer ]
│
▼
POST /approval-requests
├── agency_id, client_id, platform, campaign_id
├── amount (minor units), currency, description
├── approver_email, metadata, expiry (default: 24h)
│
▼
┌─────────────────────────────────────┐
│ ATOMIC DATABASE TRANSACTION │
└─────────────────────────────────────┘
├── INSERT
approval_requests (status: pending)
├── Generate
HMAC-SHA256 signed token (encodes request_id + expiry)
├── INSERT
outbox event: approval.requested
│
▼
┌─────────────────────────────────────┐
│ STATEFUL SINGLETON INIT │
└─────────────────────────────────────┘
├──
Approval Coordinator
│ ├── State: pending, expiry alarm set
│ └── Single source of coordination (no race conditions)
├──
Escalation Coordinator
│ ├── Load policy for (agency_id, client_id)
│ ├── If no policy → default chain: [email, 1hr timeout]
│ └── Parse quiet hours config
│
▼
Enqueue
initial notification job → Job Queue
│
▼
Return
201: { id, approval_url, expires_at }
═══════════════════════════════════════════════════════════════════════════════════
PHASE 2 — NOTIFICATION DELIVERY
═══════════════════════════════════════════════════════════════════════════════════
[ Job Queue ] ──▶
Notifier Worker
│
├── channel =
email?
│ ├── Send via
Email Automation provider
│ ├── HTML template with one-click approve/reject links
│ ├── Record: communication_attempts (sent/failed)
│ └── Write
outbox: escalation.step_sent
│
├── channel =
sms?
│ ├── Send via
Voice & SMS Engine
│ ├── Short message with approval instructions
│ ├── Record: communication_attempts
│ └── Write
outbox: escalation.step_sent
│
├── channel =
phone?
│ ├── Send SMS with call-back DID number
│ ├── "Press 1 to approve, 2 to reject"
│ ├── Create call_session (status: initiated)
│ ├── Record: communication_attempts
│ └── Write
outbox: escalation.step_sent
│
└── channel =
slack?
├── Post to designated channel/user
└── Record: communication_attempts
═══════════════════════════════════════════════════════════════════════════════════
PHASE 3 — DECISION PATHS
═══════════════════════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐
│ THREE DECISION PATHS │
└─────────────────────────────────────────────────────────────────┘
PATH A: WEB APPROVAL/REJECTION
─────────────────────────────────
[ Approver clicks link ]
│
▼
GET /approve/{token}
├── Validate
HMAC signature + expiry
├── If invalid → 400 error
└── If valid → redirect to approval UI
│
▼
[ Approver reviews details ]
├── Amount, campaign, platform, description
└── Clicks Approve or Reject
│
▼
POST /approve/{token} or POST /reject/{token}
├── Re-validate token
├── Check status == 'pending' (else 409 conflict)
│
▼
┌─────────────────────────────────────┐
│ ATOMIC DECISION TRANSACTION │
└─────────────────────────────────────┘
├── UPDATE approval_requests → status: approved/rejected
├── INSERT approval_decisions (method:
web, IP, user_agent)
├── Generate
receipt → SHA-256 hash of payload
├── INSERT immutable_receipts
├── INSERT
outbox: approval.decided
└── Store receipt copy →
Object Storage (non-blocking)
│
▼
Notify coordinators → cancel escalation, delete alarms
PATH B: PHONE (DTMF) APPROVAL/REJECTION
──────────────────────────────────────────
[ Approver calls DID number ]
│
▼
Approver presses:
1 = Approve |
2 = Reject
│
▼
POST /telephony/dtmf-callback
├── Validate
callback secret header
├── Map DTMF digit → decision
├── Check status == 'pending'
│
▼
┌─────────────────────────────────────┐
│ ATOMIC DECISION TRANSACTION │
└─────────────────────────────────────┘
├── UPDATE approval_requests → status: approved/rejected
├── INSERT approval_decisions (method:
dtmf, digit, call_id)
├── Generate
receipt → SHA-256 hash (includes DTMF context)
├── INSERT immutable_receipts
├── INSERT
outbox: approval.decided
├── UPDATE call_sessions → dtmf_received
└── Store receipt copy →
Object Storage
│
▼
Notify coordinators → cancel escalation, delete alarms
PATH C: EXPIRY (NO RESPONSE)
──────────────────────────────
[ Approval Coordinator alarm fires ]
│
▼
Status still 'pending'?
├── YES →
│ ├── UPDATE approval_requests → status: expired
│ ├── INSERT
outbox: approval.expired
│ └── Notify Escalation Coordinator → cancel
│
└── NO → Already decided, no-op
[ Poller Worker ] (runs every 5 min — safety net)
├── SELECT pending requests WHERE expires_at <= now
├── Expire any that the coordinator missed
└── Send 15-min expiry reminder emails
═══════════════════════════════════════════════════════════════════════════════════
PHASE 4 — ESCALATION ENGINE
═══════════════════════════════════════════════════════════════════════════════════
[ Escalation Coordinator ]
│
▼
┌──────────────────────────────────────────────────────┐
│ ESCALATION CHAIN (configurable per agency/client) │
│ │
│ Step 0:
Email →
[email protected] (timeout: 1hr) │
│ │ │
│ ▼ (no response after timeout) │
│ Step 1:
SMS → +1-555-0100 (timeout: 30m) │
│ │ │
│ ▼ (no response after timeout) │
│ Step 2:
Phone → +1-555-0100 (timeout: 15m) │
│ │ │
│ ▼ (no response / max attempts) │
│ Step 3:
Email →
[email protected] (timeout: 1hr) │
│ │ │
│ ▼ (all steps exhausted) │
│
Generate escalation receipt → STOP │
└──────────────────────────────────────────────────────┘
QUIET HOURS LOGIC:
├── Each step checks: is current time in quiet hours?
├── If YES → defer alarm to end of quiet hours
├── Timezone-aware (per agency/client config)
└── Day-of-week filtering supported
PHONE ATTEMPT LIMITS:
├── max_call_attempts per policy (default: 2)
├── If limit reached → skip to next step
└── Each attempt tracked in call_sessions table
═══════════════════════════════════════════════════════════════════════════════════
PHASE 5 — ENFORCEMENT
═══════════════════════════════════════════════════════════════════════════════════
[ Decision Made ]
│
├──
Approved → No enforcement needed
│
├──
Rejected →
│ ├── Enqueue enforcement job
│ └── action_type: pause_campaign / reduce_budget / notify_only
│
└──
Expired →
├── Enqueue enforcement job
└── action_type: pause_campaign (default)
│
▼
[ Executor Worker ]
├── Idempotency check (skip if already applied)
├── Execute platform action
├── Record: enforcement_actions (status: applied/failed)
├── Generate
enforcement receipt → SHA-256 hash
├── INSERT immutable_receipts
├── INSERT
outbox: enforcement.applied / enforcement.failed
└── Store receipt →
Object Storage
ROLLBACK:
├── If enforcement fails → status: failed, reason logged
├── Manual rollback supported → status: rolled_back
└── Each state transition receipted
═══════════════════════════════════════════════════════════════════════════════════
PHASE 6 — RECEIPT INTEGRITY & COMPLIANCE
═══════════════════════════════════════════════════════════════════════════════════
RECEIPT GENERATION (triggered at every decision point):
┌─────────────────────────────────────────────────┐
│ Receipt Types: │
│ ├── approval (web approve/reject) │
│ ├── rejection (web approve/reject) │
│ ├── enforcement (pause/reduce/notify) │
│ └── escalation (chain exhausted/cancelled) │
│ │
│ Each Receipt Contains: │
│ ├── Unique ID (UUID) │
│ ├── Full decision payload (JSON) │
│ ├── SHA-256 hash of payload │
│ ├── Timestamp, actor, method, IP, user-agent │
│ └── Dual-stored: SQL database + Object Storage │
└─────────────────────────────────────────────────┘
INTEGRITY VERIFICATION (on every read):
├── Fetch receipt from SQL database
├── Fetch copy from Object Storage
├── Recompute SHA-256 hash from payload
├── Compare with stored hash
├── If mismatch →
TAMPER DETECTED → flag immediately
└── Return: integrity_valid (true/false)
═══════════════════════════════════════════════════════════════════════════════════
PHASE 7 — OUTBOX & EVENT PUBLISHING
═══════════════════════════════════════════════════════════════════════════════════
TRANSACTIONAL OUTBOX PATTERN:
[ Any State Change ]
│
├── Business logic + outbox INSERT in
same transaction
│ (zero events lost, zero double-publishes)
│
▼
[ outbox table ]
├── event_type, payload_json, correlation_id
├── status: pending → sent | failed
├── attempts: 0 → max 5
└── last_attempt_at (lease mechanism)
│
▼
[ Publisher Worker ] (cron: every 60s)
├── SELECT pending entries (batch of 50)
├── Atomic claim via lease timestamp
├── Connect to
Event Stream (WebSocket)
├── Build envelope: { event_id, event_type, spoke_id, correlation_id, payload }
├── Publish to stream
├── On success → mark 'sent'
├── On failure → retry (up to 5 attempts)
└── Hub deduplicates on event_id
EVENT TYPES PUBLISHED:
├── approval.requested.v1
├── approval.decided.v1
├── approval.expired.v1
├── escalation.step_sent.v1
├── escalation.step_failed.v1
├── enforcement.applied.v1
└── enforcement.failed.v1
═══════════════════════════════════════════════════════════════════════════════════
PHASE 8 — HUB CONSUMPTION & CROSS-AGENCY GOVERNANCE
═══════════════════════════════════════════════════════════════════════════════════
[ Event Stream ] ──▶
[ NeoMoat Hub ]
│
├── Deduplicate on event_id
├── Aggregate cross-agency reporting
├── Enforce global policies
├── Detect drift (approved vs. actual spend)
└── Replay-capable: rebuild state from event history
SPOKE ISOLATION:
├── Each agency spoke operates independently
├── Hub sees events but NOT raw spend data
├── Cryptographically authenticated event envelopes
└── Zero-trust federation between spokes
═══════════════════════════════════════════════════════════════════════════════════
DATABASE SCHEMA (8 TABLES)
═══════════════════════════════════════════════════════════════════════════════════
approval_requests Primary request record (status state machine)
approval_decisions Who decided, when, how (web/dtmf/timeout)
escalation_policies Per agency/client chain + quiet hours config
communication_attempts Every notification: channel, status, error detail
call_sessions Phone call tracking: DTMF digits, call status
immutable_receipts Cryptographic receipts with hash verification
enforcement_actions Platform actions: pause/reduce/notify + rollback
outbox Transactional event queue (at-least-once delivery)