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

/var/www/vhosts/3dshawn.com/repricer.3dshawn.com/_attic/cart_add.cgi

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

Added
+338
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to b61c93be36d8
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 -- add a model to the cart.
4#
5# POST params:
6# storefront_id = which storefront's cart
7# model_id = which model to add
8# qty = quantity (default 1)
9#
10# Returns JSON { success: 1, count: N } where N is the new total
11# quantity in the cart for that storefront. Sets repricer_cart cookie
12# on first use.
13#======================================================================
14use strict;
15use warnings;
16
17use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com';
18use CGI;
19use MODS::DBConnect;
20use MODS::RePricer::Config;
21use MODS::RePricer::Cart;
22use MODS::RePricer::Experiments;
23
24$| = 1;
25
26my $q = CGI->new;
27my $db = MODS::DBConnect->new;
28my $config = MODS::RePricer::Config->new;
29my $cart = MODS::RePricer::Cart->new;
30my $DB = $config->settings('database_name');
31
32our $DONE = 0;
33END {
34 if (!$DONE) {
35 print "Content-Type: application/json\nCache-Control: no-cache\n\n";
36 print qq~{"success":0,"error":"server aborted -- check MODS/DB_ERRORS.log"}~;
37 }
38}
39
40sub fail {
41 my ($msg, $cookie_line) = @_;
42 $msg =~ s/"/\\"/g;
43 print "Content-Type: application/json\nCache-Control: no-cache\n";
44 print "Set-Cookie: $cookie_line\n" if $cookie_line;
45 print "\n";
46 print qq~{"success":0,"error":"$msg"}~;
47 $DONE = 1;
48 exit;
49}
50
51my $sid = $q->param('storefront_id') || 0;
52$sid =~ s/[^0-9]//g;
53fail('missing storefront_id') unless $sid;
54
55my $mid = $q->param('model_id') || 0;
56$mid =~ s/[^0-9]//g;
57my $bundle_id = $q->param('bundle_id') || 0;
58$bundle_id =~ s/[^0-9]//g;
59fail('missing model_id or bundle_id') unless $mid || $bundle_id;
60
61my $qty = $q->param('qty') || 1;
62$qty =~ s/[^0-9]//g;
63$qty = 1 unless $qty;
64$qty = 1 if $qty < 1;
65$qty = 99 if $qty > 99;
66
67# Optional variant params -- buyer chose "Digital file" / "Printed
68# copy" + color + material on the listing-detail page. All optional;
69# missing values resolve to digital + empty strings, which match the
70# default behavior for digital-only listings.
71my $kind = lc($q->param('kind') || '');
72$kind =~ s/[^a-z]//g;
73$kind = '' unless $kind eq 'digital' || $kind eq 'physical';
74
75sub _safe_variant {
76 my $s = shift; $s = '' unless defined $s;
77 $s =~ s/[^A-Za-z0-9 _,.\/\(\)\-]//g; # whitelist; no quotes
78 $s =~ s/^\s+|\s+$//g;
79 return substr($s, 0, 80);
80}
81my $color = _safe_variant($q->param('color') || '');
82my $material = _safe_variant($q->param('material') || '');
83
84my ($token, $cookie_line) = $cart->ensure_token($q);
85
86my $dbh = $db->db_connect();
87
88# Probe for the variant columns on cart_items -- legacy installs
89# (pre-migration) don't have them yet and we degrade to digital-only
90# behavior instead of crashing add-to-cart.
91my $have_variant_cols = 0;
92{
93 my $r = $db->db_readwrite($dbh, qq~
94 SELECT COUNT(*) AS n FROM information_schema.columns
95 WHERE table_schema='$DB' AND table_name='cart_items'
96 AND column_name='purchase_kind'
97 ~, $ENV{SCRIPT_NAME}, __LINE__);
98 $have_variant_cols = 1 if $r && $r->{n};
99}
100my $have_bundle_cols = 0;
101{
102 my $r = $db->db_readwrite($dbh, qq~
103 SELECT COUNT(*) AS n FROM information_schema.columns
104 WHERE table_schema='$DB' AND table_name='cart_items'
105 AND column_name='bundle_id'
106 ~, $ENV{SCRIPT_NAME}, __LINE__);
107 $have_bundle_cols = 1 if $r && $r->{n};
108}
109
110# Bundle branch -- if the buyer is adding a bundle, look it up,
111# verify it belongs to this storefront + is published, and insert a
112# bundle-tagged cart row. Skip the per-model logic that follows.
113if ($bundle_id) {
114 unless ($have_bundle_cols) {
115 $db->db_disconnect($dbh);
116 fail('bundles migration not run yet', $cookie_line);
117 }
118 my $bun = $db->db_readwrite($dbh, qq~
119 SELECT id, title, product_kind, base_price_cents, physical_price_cents,
120 inventory_qty, status, storefront_id
121 FROM `${DB}`.bundles
122 WHERE id='$bundle_id' AND storefront_id='$sid' LIMIT 1
123 ~, $ENV{SCRIPT_NAME}, __LINE__);
124 unless ($bun && $bun->{id} && $bun->{status} eq 'published') {
125 $db->db_disconnect($dbh);
126 fail('bundle not available', $cookie_line);
127 }
128 my $bun_kind = $bun->{product_kind} || 'digital';
129 my $eff_kind = $kind;
130 if (!$eff_kind) {
131 $eff_kind = ($bun_kind eq 'physical') ? 'physical' : 'digital';
132 }
133 if ($eff_kind eq 'physical' && $bun_kind eq 'digital') {
134 $db->db_disconnect($dbh);
135 fail('this bundle is digital-only', $cookie_line);
136 }
137 if ($eff_kind eq 'digital' && $bun_kind eq 'physical') {
138 $db->db_disconnect($dbh);
139 fail('this bundle is print-only', $cookie_line);
140 }
141 # Sold-out check for physical bundles with finite stock.
142 if ($eff_kind eq 'physical' && defined $bun->{inventory_qty}) {
143 my $inv = $bun->{inventory_qty};
144 if ($inv == 0) {
145 $db->db_disconnect($dbh);
146 fail('this bundle is sold out', $cookie_line);
147 }
148 $qty = $inv if $inv > 0 && $qty > $inv;
149 }
150 my $price = ($eff_kind eq 'physical')
151 ? ($bun->{physical_price_cents} || 0)
152 : ($bun->{base_price_cents} || 0);
153 my $color_esc = $color; $color_esc =~ s/'/''/g;
154 my $material_esc = $material; $material_esc =~ s/'/''/g;
155 # Bundle cart row -- model_id NULL, bundle_id set. The UNIQUE on
156 # cart_items (cart_token, storefront_id, model_id, kind, color, mat)
157 # ignores NULL, so two distinct bundles in the same cart will both
158 # get rows; same bundle twice bumps quantity via ON DUPLICATE for
159 # the bundle_id index match... but with NULL model_id, ON DUPLICATE
160 # won't fire and we'd get a 2nd row. Pre-check + UPDATE instead.
161 my $exist = $db->db_readwrite($dbh, qq~
162 SELECT id FROM `${DB}`.cart_items
163 WHERE cart_token='$token' AND storefront_id='$sid'
164 AND bundle_id='$bundle_id' AND purchase_kind='$eff_kind'
165 AND selected_color='$color_esc' AND selected_material='$material_esc'
166 LIMIT 1
167 ~, $ENV{SCRIPT_NAME}, __LINE__);
168 if ($exist && $exist->{id}) {
169 my $eid = $exist->{id};
170 $db->db_readwrite($dbh, qq~
171 UPDATE `${DB}`.cart_items
172 SET quantity = quantity + $qty, unit_price_cents='$price'
173 WHERE id='$eid' LIMIT 1
174 ~, $ENV{SCRIPT_NAME}, __LINE__);
175 } else {
176 $db->db_readwrite($dbh, qq~
177 INSERT INTO `${DB}`.cart_items SET
178 cart_token = '$token',
179 storefront_id = '$sid',
180 model_id = NULL,
181 bundle_id = '$bundle_id',
182 purchase_kind = '$eff_kind',
183 selected_color = '$color_esc',
184 selected_material= '$material_esc',
185 quantity = '$qty',
186 unit_price_cents = '$price',
187 added_at = NOW()
188 ~, $ENV{SCRIPT_NAME}, __LINE__);
189 }
190 my $count = $cart->count($db, $dbh, $DB, $token, $sid);
191 $db->db_disconnect($dbh);
192 print "Content-Type: application/json\nCache-Control: no-cache\n";
193 print "Set-Cookie: $cookie_line\n" if $cookie_line;
194 print "\n";
195 print qq~{"success":1,"count":$count,"bundle":1}~;
196 $DONE = 1;
197 exit;
198}
199my $have_phys_model_cols = 0;
200{
201 my $r = $db->db_readwrite($dbh, qq~
202 SELECT COUNT(*) AS n FROM information_schema.columns
203 WHERE table_schema='$DB' AND table_name='models'
204 AND column_name='product_kind'
205 ~, $ENV{SCRIPT_NAME}, __LINE__);
206 $have_phys_model_cols = 1 if $r && $r->{n};
207}
208
209# Verify the model exists, is published, and belongs to a listing on
210# this storefront. Refuse anything else -- buyers shouldn't be able to
211# put drafts or other people's purged models in their cart.
212my $phys_select = $have_phys_model_cols
213 ? q~m.product_kind, m.physical_price_cents, m.inventory_qty,~
214 : '';
215my $row = $db->db_readwrite($dbh, qq~
216 SELECT m.id, m.base_price_cents, m.status, m.purged_at,
217 $phys_select
218 sl.override_price_cents
219 FROM `${DB}`.models m
220 JOIN `${DB}`.storefront_listings sl
221 ON sl.model_id = m.id AND sl.storefront_id = '$sid' AND sl.visible = 1
222 WHERE m.id = '$mid'
223 AND m.status = 'published'
224 AND m.purged_at IS NULL
225 LIMIT 1
226~, $ENV{SCRIPT_NAME}, __LINE__);
227unless ($row && $row->{id}) {
228 $db->db_disconnect($dbh);
229 fail('model not available', $cookie_line);
230}
231
232# Resolve the effective purchase kind. A buyer who clicks Add to Cart
233# without picking a kind (e.g. from a storefront card on a 'both'
234# listing) gets the cheaper one auto-picked so a click never errors.
235my $product_kind = $have_phys_model_cols
236 ? ($row->{product_kind} || 'digital')
237 : 'digital';
238my $base_cents = $row->{override_price_cents} || $row->{base_price_cents} || 0;
239my $phys_cents = $row->{physical_price_cents} || 0;
240my $effective_kind = $kind;
241if (!$effective_kind) {
242 if ($product_kind eq 'physical') { $effective_kind = 'physical'; }
243 elsif ($product_kind eq 'both') {
244 # For 'both' with no chosen kind: prefer digital (lower
245 # friction -- no shipping needed). Buyer who wants the
246 # printed copy uses the kind toggle on the detail page.
247 $effective_kind = 'digital';
248 } else { $effective_kind = 'digital'; }
249}
250# Reject a kind the listing doesn't actually offer.
251if ($effective_kind eq 'physical' && $product_kind eq 'digital') {
252 $db->db_disconnect($dbh);
253 fail('this listing does not offer a printed copy', $cookie_line);
254}
255if ($effective_kind eq 'digital' && $product_kind eq 'physical') {
256 $db->db_disconnect($dbh);
257 fail('this listing is print-only', $cookie_line);
258}
259
260# Sold-out check for physical lines.
261if ($effective_kind eq 'physical' && defined $row->{inventory_qty}) {
262 my $inv = $row->{inventory_qty};
263 if ($inv == 0) {
264 $db->db_disconnect($dbh);
265 fail('this item is sold out', $cookie_line);
266 }
267 # If finite stock and quantity asked exceeds it, clamp.
268 if ($inv > 0 && $qty > $inv) {
269 $qty = $inv;
270 }
271}
272
273my $price = ($effective_kind eq 'physical') ? $phys_cents : $base_cents;
274
275# INSERT ... SET ... ON DUPLICATE KEY UPDATE bumps quantity if the
276# same variant of this model is already in the cart (UNIQUE on
277# cart_token + storefront + model + purchase_kind + color + material).
278if ($have_variant_cols) {
279 my $color_esc = $color; $color_esc =~ s/'/''/g;
280 my $material_esc = $material; $material_esc =~ s/'/''/g;
281 $db->db_readwrite($dbh, qq~
282 INSERT INTO `${DB}`.cart_items SET
283 cart_token = '$token',
284 storefront_id = '$sid',
285 model_id = '$mid',
286 purchase_kind = '$effective_kind',
287 selected_color = '$color_esc',
288 selected_material = '$material_esc',
289 quantity = '$qty',
290 unit_price_cents = '$price',
291 added_at = NOW()
292 ON DUPLICATE KEY UPDATE
293 quantity = quantity + $qty,
294 unit_price_cents = '$price'
295 ~, $ENV{SCRIPT_NAME}, __LINE__);
296} else {
297 # Legacy install -- variant columns missing. Fall through to the
298 # original digital-only behavior so we don't crash; the new fields
299 # land once the buyer runs the migration.
300 $db->db_readwrite($dbh, qq~
301 INSERT INTO `${DB}`.cart_items SET
302 cart_token = '$token',
303 storefront_id = '$sid',
304 model_id = '$mid',
305 quantity = '$qty',
306 unit_price_cents = '$price',
307 added_at = NOW()
308 ON DUPLICATE KEY UPDATE
309 quantity = quantity + $qty,
310 unit_price_cents = '$price'
311 ~, $ENV{SCRIPT_NAME}, __LINE__);
312}
313
314my $count = $cart->count($db, $dbh, $DB, $token, $sid);
315
316# A/B + MVT: log an add_to_cart event for every experiment this
317# visitor is bucketed into. The Experiments helper resolves
318# assignments deterministically from the visitor cookie, so the
319# event lands on the variant the buyer is actually seeing.
320my $ex = MODS::RePricer::Experiments->new;
321my $vtok = $ex->read_visitor($q);
322if ($vtok) {
323 my $vh = $ex->visitor_hash($vtok);
324 my $cents = $price * $qty;
325 $ex->log_event_for_visitor_experiments(
326 $db, $dbh, $DB, $sid, $vh, 'add_to_cart',
327 sprintf('%.2f', $cents / 100)
328 );
329}
330
331$db->db_disconnect($dbh);
332
333print "Content-Type: application/json\nCache-Control: no-cache\n";
334print "Set-Cookie: $cookie_line\n" if $cookie_line;
335print "\n";
336print qq~{"success":1,"count":$count}~;
337$DONE = 1;
338exit;
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help