Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/link_variants.cgi

O Operator
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
Restore this content →
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#======================================================================
10use strict; use warnings;
11
12use lib '/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com';
13use CGI;
14use MODS::DBConnect;
15use MODS::Login;
16use MODS::AffSoft::Config;
17use MODS::AffSoft::Wrapper;
18
19my $q = CGI->new;
20my $form = $q->Vars;
21my $auth = MODS::Login->new;
22my $wrap = MODS::AffSoft::Wrapper->new;
23my $db = MODS::DBConnect->new;
24my $cfg = MODS::AffSoft::Config->new;
25my $DB = $cfg->settings('database_name');
26
27my $userinfo = $auth->login_verify;
28unless ($userinfo) { print "Status: 302 Found\r\nLocation: /login.cgi\r\n\r\n"; exit; }
29my $uid = $userinfo->{user_id} + 0;
30my $lid = ($form->{link_id} || 0) + 0;
31unless ($lid > 0) { print "Status: 302 Found\r\nLocation: /my_links.cgi\r\n\r\n"; exit; }
32
33my $dbh = $db->db_connect;
34
35# Ownership check
36my $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__);
43unless ($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
48my $flash_kind = ''; my $flash_msg = '';
49
50if (($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
125my @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
140my $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
147my $base_url = $cfg->settings('base_url') || 'https://affiliate.3dshawn.com';
148my $link_url = "$base_url/l.cgi?t=$link->{token}";
149
150my $body = '';
151if ($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
158my $pname = _h($link->{program_name});
159my $link_url_h = _h($link_url);
160my $default_dest = _h($link->{destination_url} || '');
161my $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">&larr; Back to My Links</a></div>
166 <h1 style="margin:8px 0 4px;font-size:24px;color:#fff">A/B variants &middot; $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
179if (@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} ? ' &middot; 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 &middot; $clicks click(s) &middot; $convs conv ($rate) &middot; $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
239print "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
247sub _h {
248 my $s = shift // '';
249 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
250 return $s;
251}