Wire createCommission() into compliance batch checkout

Compliance batch orders now create commission ledger entries when
a discount code (agent referral) is used. Tracks total order amount,
discount applied, and links to the agent for payout processing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
justin 2026-05-11 10:55:40 -05:00
parent fa80c6dab9
commit 71d466c922
4 changed files with 204 additions and 0 deletions

View file

@ -1493,6 +1493,34 @@ export async function handlePaymentComplete(
} catch (err) { } catch (err) {
console.error(`[checkout] Batch ${batchId}: failed to load sub-orders:`, 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) ──────── // ── Advance compliance order workflow (queues document generation) ────────

Binary file not shown.

71
scripts/bounce-watcher.sh Normal file
View file

@ -0,0 +1,71 @@
#!/bin/bash
# Postfix bounce watcher — tails mail.log and reports bounces to Listmonk.
# Only processes bounces for campaign emails (from=<noreply@performancewest.net>).
# 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=<noreply@performancewest.net>"; 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=<info@performancewest.net>
if echo "$line" | grep -q "from=<info@performancewest.net>"; 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

View file

@ -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 = """<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>@media only screen and (max-width:600px){.pw-wrap{width:100%!important;border-radius:0!important;}.pw-pad{padding:24px 16px!important;}}body,table,td,p,a{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}table{border-collapse:collapse!important;}img{border:0;outline:none;text-decoration:none;}</style></head><body style="margin:0;padding:0;background:#eef0f3;">
<center>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#eef0f3;"><tr><td style="padding:24px 10px;">
<table role="presentation" class="pw-wrap" width="620" cellpadding="0" cellspacing="0" style="margin:0 auto;border-radius:10px;overflow:hidden;background:#fff;">
<tr><td style="background:#1a2744;padding:24px 28px;">
<img src="https://performancewest.net/images/logo.png" alt="Performance West" style="height:44px;margin-bottom:10px;display:block" />
<h1 style="color:#fff;margin:0;font-size:18px;font-weight:700;font-family:Inter,system-ui,sans-serif;">How we check your FCC compliance</h1>
<p style="color:#94a3b8;margin:6px 0 0;font-size:12px;font-family:Inter,system-ui,sans-serif;">We read your actual filed documents &mdash; here&rsquo;s what we found</p>
</td></tr>
<tr><td class="pw-pad" style="padding:28px;font-family:Inter,system-ui,sans-serif;color:#1f2937;">
<p style="font-size:15px;margin:0 0 18px;line-height:1.5;">Hi {{ .Subscriber.Name }},</p>
<p style="font-size:14px;line-height:1.7;margin:0 0 18px;">We recently sent you a compliance alert for <strong>{{ .Subscriber.Attribs.company }}</strong>. We want to be transparent about exactly how we generate those results, because <strong>we don&rsquo;t just check whether a filing exists &mdash; we download and read the actual content of your filed certification document.</strong></p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#f0f4f8;border-radius:10px;padding:18px;">
<p style="font-size:13px;color:#1e3a5f;margin:0 0 10px;font-weight:700;font-family:Inter,sans-serif;">Our audit process:</p>
<ol style="font-size:13px;color:#374151;margin:0;padding:0 0 0 18px;line-height:2.2;">
<li><strong>Download your certification PDF</strong> directly from the FCC Robocall Mitigation Database</li>
<li><strong>Extract and analyze the full text</strong> of your filed document</li>
<li><strong>Check for every section the FCC requires</strong> under 2026 enforcement standards &mdash; KYC procedures, 24-hour traceback commitment, Do-Not-Originate enforcement, material change updates, annual recertification acknowledgment, and perjury declaration</li>
<li><strong>Cross-reference</strong> against CORES registration, USAC 499, CPNI, and BDC records</li>
<li><strong>Flag specific missing sections</strong> with the exact language that needs to be added</li>
</ol>
</td></tr></table>
<p style="font-size:14px;line-height:1.7;margin:0 0 18px;">When we flag an issue, it means <strong>we read your filed document and specific required sections are not in it.</strong> This is not a guess and not based on metadata alone &mdash; it&rsquo;s based on what the FCC actually has on file for your company.</p>
<p style="font-size:14px;line-height:1.7;margin:0 0 18px;"><strong>This is not a reflection of your operations.</strong> Your company may very well be fully compliant in practice. But if your filed paperwork doesn&rsquo;t explicitly include the required language, the FCC treats it as a deficiency &mdash; and that&rsquo;s what triggers enforcement action.</p>
<p style="font-size:14px;line-height:1.7;margin:0 0 18px;">Some carriers have told us their current filing agent insists their certification is fine. We encourage you to <strong>verify for yourself</strong> &mdash; our checker pulls directly from the FCC&rsquo;s public records. If your filed document is missing required sections, it doesn&rsquo;t matter what your agent says &mdash; <strong>the FCC sees what&rsquo;s on file.</strong></p>
<p style="font-size:14px;line-height:1.7;margin:0 0 18px;">Many filing agents use older templates that were sufficient in prior years but don&rsquo;t meet the updated 2026 requirements. The fix is straightforward: refile with a certification that includes the missing language.</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#fff7ed;border:2px solid #f97316;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#9a3412;margin:0 0 6px;font-weight:600;font-family:Inter,sans-serif;">See exactly what&rsquo;s missing from your filed certification</p>
<a href="https://performancewest.net/tools/fcc-compliance-check?frn={{ .Subscriber.Attribs.fcc_frn }}" style="display:inline-block;padding:14px 40px;background:#f97316;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;font-family:Inter,sans-serif;">Free Compliance Check &rarr;</a>
</td></tr></table>
<p style="font-size:14px;line-height:1.7;margin:18px 0 0;">If your results come back clean, great &mdash; no action needed. If issues are flagged, we can help you refile quickly.</p>
<p style="font-size:14px;line-height:1.7;margin:18px 0 0;">Questions? Reply to this email or call <strong>(888) 411-0383</strong>.</p>
<p style="font-size:14px;line-height:1.7;margin:18px 0 0;">Best regards,<br><strong>Justin Hannah</strong><br>Performance West Inc.</p>
</td></tr>
<tr><td style="padding:16px 28px;background:#f8fafc;border-top:1px solid #e5e7eb;font-size:11px;color:#9ca3af;text-align:center;font-family:Inter,sans-serif;">
<p style="margin:0;">Performance West Inc. &middot; 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001 &middot; <a href="https://performancewest.net" style="color:#6b7280;">performancewest.net</a></p>
<p style="margin:6px 0 0;"><a href="{{ UnsubscribeURL }}" style="color:#6b7280;">Unsubscribe</a></p>
</td></tr>
</table>
</td></tr></table>
</center>
</body></html>"""
# 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 <noreply@performancewest.net>",
"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")