diff --git a/api/src/routes/checkout.ts b/api/src/routes/checkout.ts index 19e5bee..331574c 100644 --- a/api/src/routes/checkout.ts +++ b/api/src/routes/checkout.ts @@ -1493,6 +1493,34 @@ export async function handlePaymentComplete( } catch (err) { console.error(`[checkout] Batch ${batchId}: failed to load sub-orders:`, err); } + + // ── Commission tracking for compliance batch orders ───────────────── + try { + const discRow = await pool.query( + `SELECT DISTINCT discount_code FROM compliance_orders WHERE batch_id = $1 AND discount_code IS NOT NULL LIMIT 1`, + [batchId], + ); + const discountCode = discRow.rows[0]?.discount_code as string | null; + if (discountCode) { + const { createCommission } = await import("./agents.js"); + const totalCents = (updated.rows as any[]).reduce((sum, r) => + sum + (Number(r.service_fee_cents) || 0) + (Number(r.gov_fee_cents) || 0) - (Number(r.discount_cents) || 0), 0); + await createCommission({ + agentCode: discountCode, + orderType: "service", + orderId: 0, + orderNumber: batchId, + serviceSlug: "compliance-batch", + customerName: (order.customer_name as string) || "", + customerEmail: (order.customer_email as string) || "", + orderAmountCents: totalCents, + discountCents: (updated.rows as any[]).reduce((sum, r) => sum + (Number(r.discount_cents) || 0), 0), + }); + console.log(`[checkout] Commission created for batch ${batchId} via ${discountCode}`); + } + } catch (commErr) { + console.warn("[checkout] Commission creation failed (non-fatal):", commErr); + } } // ── Advance compliance order workflow (queues document generation) ──────── diff --git a/docs/marketing/Vantage Point Voicemail_Message.wav b/docs/marketing/Vantage Point Voicemail_Message.wav new file mode 100644 index 0000000..666dea2 Binary files /dev/null and b/docs/marketing/Vantage Point Voicemail_Message.wav differ diff --git a/scripts/bounce-watcher.sh b/scripts/bounce-watcher.sh new file mode 100644 index 0000000..a84fa13 --- /dev/null +++ b/scripts/bounce-watcher.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# Postfix bounce watcher — tails mail.log and reports bounces to Listmonk. +# Only processes bounces for campaign emails (from=). +# Ignores system cron bounces, postmaster notices, etc. +# +# Install: copy to /usr/local/bin/postfix-bounce-notify.sh +# Service: systemd unit at /etc/systemd/system/pw-bounce-watcher.service + +LOG=/var/log/mail.log +AUTH="api:6X1rKPea61N4rZ1S65Hx5zvqzbCj30F6nvEe9oVGH_Y" +API="http://localhost:9100" + +# Track queue IDs from campaign sends (from=noreply@) so we only +# report bounces for those, not for system emails. +declare -A CAMPAIGN_QIDS + +tail -F "$LOG" 2>/dev/null | while IFS= read -r line; do + + # Track queue IDs originating from campaign sender + if echo "$line" | grep -q "from="; then + QID=$(echo "$line" | sed -n 's/.*postfix\/[a-z]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p') + if [ -n "$QID" ]; then + CAMPAIGN_QIDS[$QID]=1 + fi + fi + + # Also track from= + if echo "$line" | grep -q "from="; then + QID=$(echo "$line" | sed -n 's/.*postfix\/[a-z]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p') + if [ -n "$QID" ]; then + CAMPAIGN_QIDS[$QID]=1 + fi + fi + + # Hard bounce: status=bounced + if echo "$line" | grep -q "status=bounced"; then + QID=$(echo "$line" | sed -n 's/.*postfix\/[a-z]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p') + RCPT=$(echo "$line" | sed -n 's/.*to=<\([^>]*\)>.*/\1/p') + + # Only report if this was a campaign email + if [ -n "$RCPT" ] && [ -n "${CAMPAIGN_QIDS[$QID]+x}" ]; then + curl -s -u "$AUTH" -X POST "$API/webhooks/bounce" \ + -H "Content-Type: application/json" \ + -d "{\"email\": \"$RCPT\", \"source\": \"postfix\", \"type\": \"hard\"}" \ + >/dev/null 2>&1 + logger -t bounce-notify "Hard bounce: $RCPT (qid=$QID)" + unset CAMPAIGN_QIDS[$QID] + fi + fi + + # Soft bounce with permanent 5xx in the response + if echo "$line" | grep -q "status=deferred" && echo "$line" | grep -qE "said: 5[0-9][0-9]"; then + QID=$(echo "$line" | sed -n 's/.*postfix\/[a-z]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p') + RCPT=$(echo "$line" | sed -n 's/.*to=<\([^>]*\)>.*/\1/p') + + if [ -n "$RCPT" ] && [ -n "${CAMPAIGN_QIDS[$QID]+x}" ]; then + curl -s -u "$AUTH" -X POST "$API/webhooks/bounce" \ + -H "Content-Type: application/json" \ + -d "{\"email\": \"$RCPT\", \"source\": \"postfix\", \"type\": \"soft\"}" \ + >/dev/null 2>&1 + logger -t bounce-notify "Soft bounce (5xx): $RCPT (qid=$QID)" + fi + fi + + # Clean up old QIDs when they're removed from queue + if echo "$line" | grep -q "removed$"; then + QID=$(echo "$line" | sed -n 's/.*postfix\/[a-z]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p') + unset CAMPAIGN_QIDS[$QID] 2>/dev/null + fi + +done diff --git a/scripts/methodology_campaign.py b/scripts/methodology_campaign.py new file mode 100644 index 0000000..088e840 --- /dev/null +++ b/scripts/methodology_campaign.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Create 'How we check' methodology campaign and update campaign 111.""" +import json +import urllib.request +import base64 + +AUTH = base64.b64encode(b"api:6X1rKPea61N4rZ1S65Hx5zvqzbCj30F6nvEe9oVGH_Y").decode() +API = "http://localhost:9100/api" + +BODY = """ +
+
+ + + + + + + + + +
+
+""" + +# 1. Create methodology campaign for existing recipients (List 11) +data1 = { + "name": "How we check — we read your actual filed document", + "subject": "How we check your FCC compliance (and why results may differ from your filing agent)", + "from_email": "Performance West ", + "content_type": "html", + "body": BODY, + "lists": [11], + "template_id": 1, + "headers": [{"Reply-To": "info@performancewest.net"}], + "tags": ["methodology", "transparency"], +} + +req1 = urllib.request.Request(API + "/campaigns", data=json.dumps(data1).encode(), method="POST", + headers={"Content-Type": "application/json", "Authorization": "Basic " + AUTH}) +resp1 = json.loads(urllib.request.urlopen(req1).read()) +cid1 = resp1.get("data", {}).get("id") +print(f"Campaign {cid1}: Methodology email → List 11 (1,261 existing)") + +# Start it +req_s = urllib.request.Request(API + f"/campaigns/{cid1}/status", + data=json.dumps({"status": "running"}).encode(), method="PUT", + headers={"Content-Type": "application/json", "Authorization": "Basic " + AUTH}) +urllib.request.urlopen(req_s) +print(f"Campaign {cid1} STARTED — sending to 1,261 recipients") + +# 2. Update campaign 111 body for remaining ~1,457 recipients +print("\nUpdating campaign 111 body for future recipients...") +import psycopg2 +import os +conn = psycopg2.connect(os.environ.get("DATABASE_URL", "")) +cur = conn.cursor() +cur.execute("UPDATE campaigns SET body = %s WHERE id = 111", (BODY,)) +conn.commit() +cur.close() +conn.close() +print("Campaign 111 body updated — remaining recipients will get the methodology version")