Diff -- /var/www/vhosts/3dshawn.com/repricer.3dshawn.com/price_change_detail.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/repricer.3dshawn.com/price_change_detail.cgi

added on local at 2026-07-01 21:47:16

Added
+151
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to d53739182ce5
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# RePricer -- why-did-this-price-change deep-link.
4#
5# /price_change_detail.cgi?id=N
6#
7# Reconstructs the state at the moment of a specific price_changes row:
8# - product header (SKU, title, marketplace)
9# - before / after / delta
10# - rule that fired (name, strategy, guardrails)
11# - the offers snapshot taken by the worker at that cycle
12# - the engine's plain-language explanation
13#
14# Saves the support-ticket roundtrip "why did this happen?" -- the
15# answer is literally on a page now.
16#======================================================================
17use strict;
18use warnings;
19
20use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com';
21use CGI;
22use JSON::PP;
23use MODS::DBConnect;
24use MODS::Login;
25use MODS::RePricer::Config;
26use MODS::RePricer::Wrapper;
27
28$|=1;
29my $q = CGI->new;
30my $form = $q->Vars;
31my $auth = MODS::Login->new;
32my $wrap = MODS::RePricer::Wrapper->new;
33my $db = MODS::DBConnect->new;
34my $cfg = MODS::RePricer::Config->new;
35my $DB = $cfg->settings('database_name');
36
37my $userinfo = $auth->login_verify();
38unless ($userinfo) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
39my $uid = $userinfo->{user_id}; $uid =~ s/[^0-9]//g;
40
41sub _h { my $s = shift // ''; $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g; $s }
42sub _money { my $c = shift; (defined $c && length $c) ? sprintf('$%.2f', $c / 100) : '--'; }
43
44my $cid = $form->{id} || 0; $cid =~ s/[^0-9]//g;
45unless ($cid) { print "Status: 302 Found\nLocation: /products.cgi\n\n"; exit; }
46
47my $dbh = $db->db_connect();
48my $row = $db->db_readwrite($dbh, qq~
49 SELECT pc.*, p.sku, p.title, p.marketplace, p.asin, p.walmart_item_id,
50 r.name AS rule_name, r.strategy, r.scope,
51 r.min_price_cents AS rule_floor,
52 r.max_price_cents AS rule_ceiling,
53 r.step_amount_cents AS rule_step,
54 r.cooldown_minutes,
55 r.low_stock_threshold, r.low_stock_action_pct
56 FROM `${DB}`.price_changes pc
57 JOIN `${DB}`.products p ON p.id = pc.product_id
58 LEFT JOIN `${DB}`.repricing_rules r ON r.id = pc.rule_id
59 WHERE pc.id='$cid' AND pc.user_id='$uid'
60 LIMIT 1
61~, $ENV{SCRIPT_NAME}, __LINE__);
62$db->db_disconnect($dbh);
63
64unless ($row && $row->{id}) {
65 print "Status: 302 Found\nLocation: /products.cgi?err=notfound\n\n"; exit;
66}
67
68my $offers = [];
69if ($row->{offers_snapshot} && length $row->{offers_snapshot}) {
70 my $parsed = eval { JSON::PP::decode_json($row->{offers_snapshot}) };
71 $offers = ($parsed && ref $parsed eq 'HASH' && $parsed->{offers})
72 ? $parsed->{offers} : [];
73}
74
75my $delta = ($row->{new_price_cents} // 0) - ($row->{old_price_cents} // 0);
76my $dcol = $delta < 0 ? '#10b981' : ($delta > 0 ? '#fbbf24' : '#7e92b6');
77my $sync_col = $row->{sync_status} eq 'synced' ? '#10b981'
78 : $row->{sync_status} eq 'failed' ? '#fca5a5' : '#fbbf24';
79
80my $body = '<div style="max-width:900px;margin:0 auto;padding:24px 28px">';
81$body .= '<div style="font-size:12px;color:#7e92b6;margin-bottom:8px">'
82 . '<a href="/product.cgi?id=' . $row->{product_id} . '" style="color:#7e92b6">&larr; ' . _h($row->{sku}) . ' / ' . _h(ucfirst $row->{marketplace}) . '</a></div>';
83$body .= '<h1 style="font-size:24px;color:#fff;margin:0 0 6px;font-weight:700">Why did this price change?</h1>';
84$body .= '<p style="color:#7e92b6;font-size:14px;margin:0 0 18px">Reconstructed state at the moment of price change #' . _h($cid) . '. The competitor offers below are a snapshot captured by the worker on that cycle &mdash; not live data.</p>';
85
86# Summary panel
87$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:14px">';
88$body .= ' <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px">';
89$body .= ' <div><div style="color:#7e92b6;font-size:11px;text-transform:uppercase;letter-spacing:1px">When</div><div style="color:#e6ecf6;font-size:14px;margin-top:4px">' . _h($row->{created_at}) . '</div></div>';
90$body .= ' <div><div style="color:#7e92b6;font-size:11px;text-transform:uppercase;letter-spacing:1px">Old price</div><div style="color:#e6ecf6;font-size:18px;font-weight:700;margin-top:4px">' . _money($row->{old_price_cents}) . '</div></div>';
91$body .= ' <div><div style="color:#7e92b6;font-size:11px;text-transform:uppercase;letter-spacing:1px">New price</div><div style="color:' . $dcol . ';font-size:18px;font-weight:700;margin-top:4px">' . _money($row->{new_price_cents}) . '</div></div>';
92$body .= ' <div><div style="color:#7e92b6;font-size:11px;text-transform:uppercase;letter-spacing:1px">Sync</div><div style="color:' . $sync_col . ';font-size:14px;font-weight:600;margin-top:4px">' . _h($row->{sync_status}) . '</div></div>';
93$body .= ' </div>';
94if ($row->{sync_error}) {
95 $body .= ' <div style="margin-top:12px;color:#fca5a5;font-size:13px;background:#7f1d1d22;padding:10px;border-radius:6px">Sync error: ' . _h($row->{sync_error}) . '</div>';
96}
97$body .= '</section>';
98
99# Rule details
100$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:14px">';
101$body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#7e92b6;text-transform:uppercase;font-weight:600;margin-bottom:10px">Rule that fired</div>';
102if ($row->{rule_id} && $row->{rule_name}) {
103 $body .= ' <div style="color:#e6ecf6;font-size:16px;font-weight:600">' . _h($row->{rule_name}) . '</div>';
104 $body .= ' <div style="color:#14b8a6;font-size:13px;margin-top:2px">strategy: ' . _h($row->{strategy}) . ' &middot; scope: ' . _h($row->{scope}) . '</div>';
105 $body .= ' <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:12px;font-size:13px">';
106 $body .= ' <div><span style="color:#7e92b6">Floor:</span> <b>' . _money($row->{rule_floor}) . '</b></div>';
107 $body .= ' <div><span style="color:#7e92b6">Ceiling:</span> <b>' . _money($row->{rule_ceiling}) . '</b></div>';
108 $body .= ' <div><span style="color:#7e92b6">Step:</span> <b>' . _money($row->{rule_step}) . '</b></div>';
109 $body .= ' <div><span style="color:#7e92b6">Cooldown:</span> <b>' . _h($row->{cooldown_minutes} // 0) . ' min</b></div>';
110 $body .= ' </div>';
111} elsif ($row->{automated}) {
112 $body .= ' <div style="color:#fbbf24;font-size:14px">Engine fired but the rule has since been deleted. The change reason was: "' . _h($row->{reason}) . '"</div>';
113} else {
114 $body .= ' <div style="color:#7e92b6;font-size:14px">Manual change (no rule).</div>';
115}
116if ($row->{rule_explanation}) {
117 $body .= ' <div style="margin-top:14px;padding:12px;background:#0a0f1f;border:1px solid #1f2a4a;border-radius:6px;font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#14b8a6">' . _h($row->{rule_explanation}) . '</div>';
118}
119$body .= '</section>';
120
121# Competitor offers snapshot
122$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:14px">';
123$body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#7e92b6;text-transform:uppercase;font-weight:600;margin-bottom:10px">Competitor offers at change time</div>';
124if (@$offers) {
125 $body .= ' <table style="width:100%;font-size:13px;color:#e6ecf6"><thead><tr style="color:#7e92b6;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:1px"><th style="padding:6px">Seller</th><th style="padding:6px">Price</th><th style="padding:6px">Ship</th><th style="padding:6px">Landed</th><th style="padding:6px">FBA</th><th style="padding:6px">Buy Box</th></tr></thead><tbody>';
126 foreach my $o (@$offers) {
127 my $pr = $o->{price_cents} // 0;
128 my $sh = $o->{ship_cents} // 0;
129 my $lan = $pr + $sh;
130 my $bx = $o->{is_buybox} ? '<span style="color:#10b981">&check; Yes</span>' : '<span style="color:#7e92b6">No</span>';
131 my $fb = $o->{is_fba} ? '<span style="color:#14b8a6">FBA</span>' : '<span style="color:#7e92b6">FBM</span>';
132 $body .= '<tr style="border-top:1px solid #1f2a4a">';
133 $body .= '<td style="padding:8px 6px;font-family:ui-monospace,Menlo,monospace;color:#cbd5e1">' . _h(substr($o->{seller_name} // $o->{seller_id} // '?', 0, 40)) . '</td>';
134 $body .= '<td style="padding:8px 6px">' . _money($pr) . '</td>';
135 $body .= '<td style="padding:8px 6px">' . _money($sh) . '</td>';
136 $body .= '<td style="padding:8px 6px"><b>' . _money($lan) . '</b></td>';
137 $body .= '<td style="padding:8px 6px">' . $fb . '</td>';
138 $body .= '<td style="padding:8px 6px">' . $bx . '</td>';
139 $body .= '</tr>';
140 }
141 $body .= '</tbody></table>';
142} else {
143 $body .= ' <div style="color:#7e92b6;font-size:13px">No offers snapshot was captured for this change (predates the snapshot column, OR the change was manual).</div>';
144}
145$body .= '</section>';
146
147$body .= '</div>';
148
149print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
150$wrap->render({ userinfo => $userinfo, page_key => 'products', title => 'Price change detail', body => $body });
151exit;