Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/link_variants.cgi
Diff
/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/link_variants.cgi
added on local at 2026-07-01 13:47:23
Added
+251
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 9531448e9e63
to 9531448e9e63
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # AffSoft -- affiliate: link variants (A/B testing). | |
| 4 | # | |
| 5 | # GET /link_variants.cgi?link_id=N -> list variants + per-variant stats | |
| 6 | # POST act=create -> add a variant | |
| 7 | # POST act=update -> rename / change destination / change weight / toggle active | |
| 8 | # POST act=delete -> remove a variant | |
| 9 | #====================================================================== | |
| 10 | use strict; use warnings; | |
| 11 | ||
| 12 | use lib '/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com'; | |
| 13 | use CGI; | |
| 14 | use MODS::DBConnect; | |
| 15 | use MODS::Login; | |
| 16 | use MODS::AffSoft::Config; | |
| 17 | use MODS::AffSoft::Wrapper; | |
| 18 | ||
| 19 | my $q = CGI->new; | |
| 20 | my $form = $q->Vars; | |
| 21 | my $auth = MODS::Login->new; | |
| 22 | my $wrap = MODS::AffSoft::Wrapper->new; | |
| 23 | my $db = MODS::DBConnect->new; | |
| 24 | my $cfg = MODS::AffSoft::Config->new; | |
| 25 | my $DB = $cfg->settings('database_name'); | |
| 26 | ||
| 27 | my $userinfo = $auth->login_verify; | |
| 28 | unless ($userinfo) { print "Status: 302 Found\r\nLocation: /login.cgi\r\n\r\n"; exit; } | |
| 29 | my $uid = $userinfo->{user_id} + 0; | |
| 30 | my $lid = ($form->{link_id} || 0) + 0; | |
| 31 | unless ($lid > 0) { print "Status: 302 Found\r\nLocation: /my_links.cgi\r\n\r\n"; exit; } | |
| 32 | ||
| 33 | my $dbh = $db->db_connect; | |
| 34 | ||
| 35 | # Ownership check | |
| 36 | my $link = $db->db_readwrite($dbh, qq~ | |
| 37 | SELECT l.id, l.token, l.program_id, l.affiliate_user_id, | |
| 38 | p.name AS program_name, p.destination_url | |
| 39 | FROM ${DB}.affiliate_links l | |
| 40 | JOIN ${DB}.affiliate_programs p ON p.id = l.program_id | |
| 41 | WHERE l.id = $lid LIMIT 1 | |
| 42 | ~, $0, __LINE__); | |
| 43 | unless ($link && $link->{affiliate_user_id} == $uid) { | |
| 44 | $db->db_disconnect($dbh); | |
| 45 | print "Status: 403 Forbidden\r\nContent-Type: text/plain\r\n\r\nNot your link.\n"; exit; | |
| 46 | } | |
| 47 | ||
| 48 | my $flash_kind = ''; my $flash_msg = ''; | |
| 49 | ||
| 50 | if (($ENV{REQUEST_METHOD} || '') eq 'POST') { | |
| 51 | my $act = $form->{act} || ''; | |
| 52 | ||
| 53 | if ($act eq 'create' || $act eq 'update') { | |
| 54 | my $vid = ($form->{variant_id} || 0) + 0; | |
| 55 | my $label = $form->{label} || ''; | |
| 56 | $label =~ s/^\s+|\s+$//g; $label = substr($label, 0, 120); | |
| 57 | my $dest = $form->{destination_url} || ''; | |
| 58 | $dest =~ s/^\s+|\s+$//g; $dest = substr($dest, 0, 500); | |
| 59 | my $sub_id = $form->{sub_id} || ''; $sub_id = substr($sub_id, 0, 120); | |
| 60 | my $weight = ($form->{weight} || 100) + 0; | |
| 61 | $weight = 0 if $weight < 0; $weight = 255 if $weight > 255; | |
| 62 | my $active = $form->{is_active} ? 1 : 0; | |
| 63 | my $is_control = $form->{is_control} ? 1 : 0; | |
| 64 | ||
| 65 | my $label_q = $label; $label_q =~ s/'/\\'/g; | |
| 66 | my $dest_q = $dest; $dest_q =~ s/'/\\'/g; | |
| 67 | my $sub_q = $sub_id; $sub_q =~ s/'/\\'/g; | |
| 68 | my $dest_sql = length($dest) ? "'$dest_q'" : 'NULL'; | |
| 69 | my $sub_sql = length($sub_id) ? "'$sub_q'" : 'NULL'; | |
| 70 | ||
| 71 | unless (length $label) { | |
| 72 | $flash_kind = 'err'; $flash_msg = 'Label is required.'; | |
| 73 | } | |
| 74 | elsif ($act eq 'create') { | |
| 75 | $db->db_readwrite($dbh, qq~ | |
| 76 | INSERT INTO ${DB}.affiliate_link_variants SET | |
| 77 | link_id = $lid, | |
| 78 | label = '$label_q', | |
| 79 | destination_url = $dest_sql, | |
| 80 | sub_id = $sub_sql, | |
| 81 | weight = $weight, | |
| 82 | is_active = $active, | |
| 83 | is_control = $is_control, | |
| 84 | created_at = NOW() | |
| 85 | ~, $0, __LINE__); | |
| 86 | $flash_kind = 'ok'; $flash_msg = "Variant '$label' created."; | |
| 87 | } | |
| 88 | elsif ($act eq 'update' && $vid > 0) { | |
| 89 | $db->db_readwrite($dbh, qq~ | |
| 90 | UPDATE ${DB}.affiliate_link_variants SET | |
| 91 | label = '$label_q', | |
| 92 | destination_url = $dest_sql, | |
| 93 | sub_id = $sub_sql, | |
| 94 | weight = $weight, | |
| 95 | is_active = $active, | |
| 96 | is_control = $is_control | |
| 97 | WHERE id = $vid AND link_id = $lid LIMIT 1 | |
| 98 | ~, $0, __LINE__); | |
| 99 | $flash_kind = 'ok'; $flash_msg = "Variant updated."; | |
| 100 | } | |
| 101 | } | |
| 102 | elsif ($act eq 'delete') { | |
| 103 | my $vid = ($form->{variant_id} || 0) + 0; | |
| 104 | if ($vid > 0) { | |
| 105 | $db->db_readwrite($dbh, qq~ | |
| 106 | DELETE FROM ${DB}.affiliate_link_variants | |
| 107 | WHERE id = $vid AND link_id = $lid LIMIT 1 | |
| 108 | ~, $0, __LINE__); | |
| 109 | $flash_kind = 'ok'; $flash_msg = 'Variant removed. Clicks/conversions on it stay attributed for reporting.'; | |
| 110 | } | |
| 111 | } | |
| 112 | elsif ($act eq 'toggle_active') { | |
| 113 | my $vid = ($form->{variant_id} || 0) + 0; | |
| 114 | if ($vid > 0) { | |
| 115 | $db->db_readwrite($dbh, qq~ | |
| 116 | UPDATE ${DB}.affiliate_link_variants | |
| 117 | SET is_active = NOT is_active | |
| 118 | WHERE id = $vid AND link_id = $lid LIMIT 1 | |
| 119 | ~, $0, __LINE__); | |
| 120 | } | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | # Pull variants + their per-variant stats | |
| 125 | my @variants = $db->db_readwrite_multiple($dbh, qq~ | |
| 126 | SELECT v.id, v.label, v.destination_url, v.sub_id, v.weight, | |
| 127 | v.is_active, v.is_control, | |
| 128 | (SELECT COUNT(*) FROM ${DB}.affiliate_clicks ac | |
| 129 | WHERE ac.variant_id = v.id) AS clicks, | |
| 130 | (SELECT COUNT(*) FROM ${DB}.affiliate_conversions ac2 | |
| 131 | WHERE ac2.variant_id = v.id) AS conversions, | |
| 132 | (SELECT COALESCE(SUM(ac2.commission_cents), 0) FROM ${DB}.affiliate_conversions ac2 | |
| 133 | WHERE ac2.variant_id = v.id AND ac2.status IN ('approved','paid')) AS earned_cents | |
| 134 | FROM ${DB}.affiliate_link_variants v | |
| 135 | WHERE v.link_id = $lid | |
| 136 | ORDER BY v.is_active DESC, v.id | |
| 137 | ~, $0, __LINE__); | |
| 138 | ||
| 139 | # Control row (no variants set) clicks | |
| 140 | my $no_variant_stats = $db->db_readwrite($dbh, qq~ | |
| 141 | SELECT COUNT(*) AS clicks FROM ${DB}.affiliate_clicks | |
| 142 | WHERE link_id = $lid AND variant_id IS NULL | |
| 143 | ~, $0, __LINE__); | |
| 144 | ||
| 145 | $db->db_disconnect($dbh); | |
| 146 | ||
| 147 | my $base_url = $cfg->settings('base_url') || 'https://affiliate.3dshawn.com'; | |
| 148 | my $link_url = "$base_url/l.cgi?t=$link->{token}"; | |
| 149 | ||
| 150 | my $body = ''; | |
| 151 | if ($flash_msg) { | |
| 152 | my $col = $flash_kind eq 'ok' ? '#22c55e' : '#ef4444'; | |
| 153 | my $bg = $flash_kind eq 'ok' ? 'rgba(34,197,94,.08)' : 'rgba(239,68,68,.08)'; | |
| 154 | my $brd = $flash_kind eq 'ok' ? 'rgba(34,197,94,.35)' : 'rgba(239,68,68,.35)'; | |
| 155 | $body .= qq{<div style="padding:14px 16px;border:1px solid $brd;background:$bg;color:$col;border-radius:10px;margin-bottom:24px;font-size:14px">$flash_msg</div>}; | |
| 156 | } | |
| 157 | ||
| 158 | my $pname = _h($link->{program_name}); | |
| 159 | my $link_url_h = _h($link_url); | |
| 160 | my $default_dest = _h($link->{destination_url} || ''); | |
| 161 | my $no_var_clicks = $no_variant_stats->{clicks} || 0; | |
| 162 | ||
| 163 | $body .= qq~ | |
| 164 | <div class="page-pad" style="padding:0"> | |
| 165 | <div style="margin-bottom:8px"><a href="/my_links.cgi" style="color:#9ca3af;font-size:13px;text-decoration:none">← Back to My Links</a></div> | |
| 166 | <h1 style="margin:8px 0 4px;font-size:24px;color:#fff">A/B variants · $pname</h1> | |
| 167 | <p style="color:#9ca3af;margin:0 0 24px;font-size:14px">Test multiple destination URLs or sub_ids on the same tracking link. Clicks rotate weighted-random across active variants. Stats roll up per-variant.</p> | |
| 168 | ||
| 169 | <div style="background:linear-gradient(135deg, rgba(91,33,182,.10), rgba(46,16,101,.04));border:1px solid rgba(91,33,182,.35);border-radius:12px;padding:16px 18px;margin-bottom:24px"> | |
| 170 | <div style="font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#8b5cf6;font-weight:600;margin-bottom:6px">Your tracking link</div> | |
| 171 | <div style="font-family:ui-monospace,Consolas,Menlo,monospace;font-size:13px;color:#fff;word-break:break-all">$link_url_h</div> | |
| 172 | <div style="font-size:11px;color:#9ca3af;margin-top:6px">$no_var_clicks click(s) recorded WITHOUT a variant (before any variant was active)</div> | |
| 173 | </div> | |
| 174 | ||
| 175 | <div style="background:rgba(20,20,30,.4);border:1px solid rgba(255,255,255,.06);border-radius:14px;overflow:hidden;margin-bottom:24px"> | |
| 176 | <div style="padding:12px 18px;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#8b5cf6;font-weight:600;background:rgba(91,33,182,.05)">Variants (${\ scalar(@variants) })</div> | |
| 177 | ~; | |
| 178 | ||
| 179 | if (@variants) { | |
| 180 | for my $v (@variants) { | |
| 181 | my $label = _h($v->{label}); | |
| 182 | my $dest = _h($v->{destination_url} || '(uses program default)'); | |
| 183 | my $sub = $v->{sub_id} ? ' · sub_id=' . _h($v->{sub_id}) : ''; | |
| 184 | my $w = $v->{weight}; | |
| 185 | my $clicks = $v->{clicks} || 0; | |
| 186 | my $convs = $v->{conversions} || 0; | |
| 187 | my $earned = sprintf('$%.2f', ($v->{earned_cents} || 0) / 100); | |
| 188 | my $rate = $clicks > 0 ? sprintf('%.1f%%', 100 * $convs / $clicks) : '-'; | |
| 189 | my $active_chip = $v->{is_active} | |
| 190 | ? '<span style="background:rgba(34,197,94,.15);color:#22c55e;font-size:10px;padding:2px 8px;border-radius:99px;font-weight:600">ACTIVE</span>' | |
| 191 | : '<span style="background:rgba(255,255,255,.06);color:#9ca3af;font-size:10px;padding:2px 8px;border-radius:99px;font-weight:600">PAUSED</span>'; | |
| 192 | my $control_chip = $v->{is_control} | |
| 193 | ? ' <span style="background:rgba(91,33,182,.18);color:#8b5cf6;font-size:10px;padding:2px 8px;border-radius:99px;font-weight:600">CONTROL</span>' : ''; | |
| 194 | $body .= qq~ | |
| 195 | <div style="padding:16px 18px;border-top:1px solid rgba(255,255,255,.06);display:grid;grid-template-columns:1fr auto;gap:16px;align-items:center"> | |
| 196 | <div> | |
| 197 | <div style="display:flex;gap:8px;align-items:baseline;flex-wrap:wrap;margin-bottom:4px"> | |
| 198 | <span style="font-size:15px;color:#fff;font-weight:600">$label</span> | |
| 199 | $active_chip$control_chip | |
| 200 | </div> | |
| 201 | <div style="font-size:12px;color:#9ca3af;margin-bottom:4px;word-break:break-all">$dest$sub</div> | |
| 202 | <div style="font-size:12px;color:#e5e7eb">weight $w · $clicks click(s) · $convs conv ($rate) · $earned earned</div> | |
| 203 | </div> | |
| 204 | <div style="display:flex;gap:6px"> | |
| 205 | <form method="POST" style="display:inline"><input type="hidden" name="link_id" value="$lid"><input type="hidden" name="act" value="toggle_active"><input type="hidden" name="variant_id" value="$v->{id}"><button type="submit" style="padding:6px 12px;background:transparent;border:1px solid rgba(255,255,255,.15);border-radius:6px;color:#e5e7eb;font-size:12px;cursor:pointer">${\ ($v->{is_active} ? 'Pause' : 'Activate') }</button></form> | |
| 206 | <form method="POST" style="display:inline" onsubmit="return confirm('Delete this variant? Past clicks/conversions stay attributed.')"><input type="hidden" name="link_id" value="$lid"><input type="hidden" name="act" value="delete"><input type="hidden" name="variant_id" value="$v->{id}"><button type="submit" style="padding:6px 12px;background:transparent;border:1px solid rgba(239,68,68,.4);border-radius:6px;color:#ef4444;font-size:12px;cursor:pointer">Delete</button></form> | |
| 207 | </div> | |
| 208 | </div> | |
| 209 | ~; | |
| 210 | } | |
| 211 | } else { | |
| 212 | $body .= '<div style="padding:24px;text-align:center;color:#9ca3af;font-size:13px">No variants yet. Add one below to start A/B testing.</div>'; | |
| 213 | } | |
| 214 | $body .= '</div>'; | |
| 215 | ||
| 216 | # Add form | |
| 217 | $body .= qq~ | |
| 218 | <div style="background:rgba(20,20,30,.4);border:1px solid rgba(255,255,255,.06);border-radius:14px;padding:24px"> | |
| 219 | <h2 style="font-size:16px;color:#fff;margin:0 0 16px">Add a variant</h2> | |
| 220 | <form method="POST"> | |
| 221 | <input type="hidden" name="link_id" value="$lid"> | |
| 222 | <input type="hidden" name="act" value="create"> | |
| 223 | <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px"> | |
| 224 | <label><div style="font-size:12px;color:#9ca3af;margin-bottom:6px">Label</div><input type="text" name="label" required maxlength="120" placeholder="e.g. landing-blue-cta" style="width:100%;padding:10px 12px;background:#0f1014;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#fff;font-size:14px;box-sizing:border-box"></label> | |
| 225 | <label><div style="font-size:12px;color:#9ca3af;margin-bottom:6px">Weight (0-255)</div><input type="number" name="weight" value="100" min="0" max="255" style="width:100%;padding:10px 12px;background:#0f1014;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#fff;font-size:14px;box-sizing:border-box"></label> | |
| 226 | </div> | |
| 227 | <label style="display:block;margin-bottom:14px"><div style="font-size:12px;color:#9ca3af;margin-bottom:6px">Destination URL (overrides program default; leave blank to use $default_dest)</div><input type="url" name="destination_url" placeholder="https://example.com/landing-b" style="width:100%;padding:10px 12px;background:#0f1014;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#fff;font-size:14px;box-sizing:border-box"></label> | |
| 228 | <label style="display:block;margin-bottom:14px"><div style="font-size:12px;color:#9ca3af;margin-bottom:6px">Sub-ID (appended as ?sub_id=... so the merchant can see which variant)</div><input type="text" name="sub_id" maxlength="120" placeholder="blue-cta" style="width:100%;padding:10px 12px;background:#0f1014;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#fff;font-size:14px;box-sizing:border-box"></label> | |
| 229 | <div style="display:flex;gap:20px;margin-bottom:18px;font-size:13px;color:#e5e7eb"> | |
| 230 | <label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="is_active" value="1" checked>Active immediately</label> | |
| 231 | <label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="is_control" value="1">Mark as control</label> | |
| 232 | </div> | |
| 233 | <div style="display:flex;justify-content:flex-end"><button type="submit" style="padding:10px 22px;background:#5b21b6;border:none;border-radius:8px;color:#fff;font-size:14px;font-weight:600;cursor:pointer">Add variant</button></div> | |
| 234 | </form> | |
| 235 | </div> | |
| 236 | </div> | |
| 237 | ~; | |
| 238 | ||
| 239 | print "Content-Type: text/html; charset=utf-8\r\nCache-Control: no-cache\r\n\r\n"; | |
| 240 | $wrap->render({ | |
| 241 | userinfo => $userinfo, | |
| 242 | page_key => 'my_links', | |
| 243 | title => "A/B variants \xe2\x80\x94 $link->{program_name}", | |
| 244 | body => $body, | |
| 245 | }); | |
| 246 | ||
| 247 | sub _h { | |
| 248 | my $s = shift // ''; | |
| 249 | $s =~ s/&/&/g; $s =~ s/</</g; $s =~ s/>/>/g; $s =~ s/"/"/g; $s =~ s/'/'/g; | |
| 250 | return $s; | |
| 251 | } |