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:
parent
fa80c6dab9
commit
71d466c922
4 changed files with 204 additions and 0 deletions
|
|
@ -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) ────────
|
||||||
|
|
|
||||||
BIN
docs/marketing/Vantage Point Voicemail_Message.wav
Normal file
BIN
docs/marketing/Vantage Point Voicemail_Message.wav
Normal file
Binary file not shown.
71
scripts/bounce-watcher.sh
Normal file
71
scripts/bounce-watcher.sh
Normal 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
|
||||||
105
scripts/methodology_campaign.py
Normal file
105
scripts/methodology_campaign.py
Normal 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 — here’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’t just check whether a filing exists — 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 — 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 — it’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’t explicitly include the required language, the FCC treats it as a deficiency — and that’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> — our checker pulls directly from the FCC’s public records. If your filed document is missing required sections, it doesn’t matter what your agent says — <strong>the FCC sees what’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’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’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 →</a>
|
||||||
|
</td></tr></table>
|
||||||
|
|
||||||
|
<p style="font-size:14px;line-height:1.7;margin:18px 0 0;">If your results come back clean, great — 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. · 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001 · <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")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue