Diff -- /var/www/vhosts/webstls.com/httpdocs/cart.cgi
Diff

/var/www/vhosts/webstls.com/httpdocs/cart.cgi

added on WebSTLs (webstls.com) at 2026-07-01 22:26:29

Added
+656
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to f269533cdb99
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# WebSTLs -- cart view + add/remove/promo (consolidated).
4#
5# GET /cart.cgi?id=<storefront_id> -- render the cart page
6# POST /cart_add.cgi -- add a model or bundle (JSON)
7# POST /cart_remove.cgi -- remove a cart_items row (JSON)
8# POST /cart_promo.cgi -- apply/remove a promo code (redirect)
9#
10# The former cart_add.cgi, cart_remove.cgi, and cart_promo.cgi are now
11# 3-line wrappers that `do` this file. Dispatch is on SCRIPT_NAME.
12#======================================================================
13use strict;
14use warnings;
15
16use lib '/var/www/vhosts/webstls.com/httpdocs';
17use CGI;
18use MODS::Template;
19use MODS::DBConnect;
20use MODS::WebSTLs::Config;
21use MODS::WebSTLs::Themes;
22use MODS::WebSTLs::Cart;
23use MODS::WebSTLs::Promotions;
24use MODS::WebSTLs::BuyerAuth;
25use MODS::WebSTLs::BuyerCredits;
26use MODS::WebSTLs::Experiments;
27
28$| = 1;
29
30my $q = CGI->new;
31my $form = $q->Vars;
32my $db = MODS::DBConnect->new;
33my $config = MODS::WebSTLs::Config->new;
34my $cart = MODS::WebSTLs::Cart->new;
35my $DB = $config->settings('database_name');
36
37my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'cart';
38
39if ($entry eq 'cart_add') {
40 _cart_add_main();
41 exit;
42}
43if ($entry eq 'cart_remove') {
44 _cart_remove_main();
45 exit;
46}
47if ($entry eq 'cart_promo') {
48 _cart_promo_main();
49 exit;
50}
51
52# ---- default: GET cart.cgi (render page) -----------------------------
53my $tfile = MODS::Template->new;
54my $themes = MODS::WebSTLs::Themes->new;
55
56my $sid = $form->{id} || $form->{storefront_id} || 0;
57$sid =~ s/[^0-9]//g;
58unless ($sid) {
59 print "Status: 302 Found\nLocation: /\n\n";
60 exit;
61}
62
63my $dbh = $db->db_connect();
64
65# Pull the storefront so the cart page can render in its theme.
66my $store = $db->db_readwrite($dbh,
67 qq~SELECT id, name, subdomain, theme_id FROM `${DB}`.storefronts WHERE id='$sid' AND status='live' LIMIT 1~,
68 $ENV{SCRIPT_NAME}, __LINE__);
69unless ($store && $store->{id}) {
70 $db->db_disconnect($dbh);
71 print "Status: 302 Found\nLocation: /\n\n";
72 exit;
73}
74
75my $theme = $themes->by_id($store->{theme_id});
76my $css_vars = $themes->css_vars($theme);
77
78my ($token, $cookie_line) = $cart->ensure_token($q);
79my @rows = $cart->items($db, $dbh, $DB, $token, $sid);
80my $subtotal = $cart->total_cents($db, $dbh, $DB, $token, $sid);
81
82# Promo state: an applied cart-promo lives in cart_promotions; we also
83# surface the platform's currently-marquee'd 'model' promo (if any) so
84# buyers see what's on offer before they even type a code.
85my $promo = MODS::WebSTLs::Promotions->new;
86my $applied = $promo->cart_applied($db, $dbh, $DB, $token, $sid);
87my $discount = $applied ? $promo->apply_to_total_cents($applied, $subtotal) : 0;
88my $total = $subtotal - $discount;
89$total = 0 if $total < 0;
90
91# Store credit available to this buyer (registered only -- guests can
92# see "Sign in to use your $X store credit" via a hint surfaced when
93# the email field is filled on the checkout page, not here).
94my $buyer_auth = MODS::WebSTLs::BuyerAuth->new;
95my $bc = MODS::WebSTLs::BuyerCredits->new;
96my $credit_cents = 0;
97my $credit_apply = 0; # how much of the current cart will be covered
98my $buyer_signed = 0;
99{
100 my $b = $buyer_auth->verify($q, $db, $dbh, $DB);
101 if ($b && $b->{id} && $b->{storefront_id} && $b->{storefront_id} eq $sid) {
102 $buyer_signed = 1;
103 $credit_cents = $bc->balance_cents($db, $dbh, $DB, $sid,
104 { buyer_account_id => $b->{id} });
105 if ($credit_cents > 0 && $total > 0) {
106 $credit_apply = $credit_cents > $total ? $total : $credit_cents;
107 }
108 }
109}
110my $total_after_credit = $total - $credit_apply;
111$total_after_credit = 0 if $total_after_credit < 0;
112
113$db->db_disconnect($dbh);
114
115my $applied_code = $applied ? ($applied->{code} || '') : '';
116my $applied_label = $applied ? $promo->marketing_label($applied) : '';
117
118# Flash from /cart_promo.cgi redirects (?promo_ok=... / ?promo_err=...).
119my $promo_flash_msg = '';
120my $promo_flash_kind = '';
121if (defined $form->{promo_ok}) {
122 $promo_flash_kind = 'ok';
123 $promo_flash_msg = $form->{promo_ok};
124} elsif (defined $form->{promo_err}) {
125 $promo_flash_kind = 'err';
126 $promo_flash_msg = $form->{promo_err};
127}
128$promo_flash_msg =~ s/[^\x20-\x7E]//g;
129$promo_flash_msg = substr($promo_flash_msg, 0, 200);
130
131my @items;
132my $needs_shipping = 0;
133foreach my $r (@rows) {
134 my $hero = $r->{hero_image_url} || '/assets/no-cover.svg';
135 my $price = '$' . sprintf('%.2f', ($r->{unit_price_cents} || 0) / 100);
136 my $line = '$' . sprintf('%.2f', (($r->{unit_price_cents} || 0) * ($r->{quantity} || 1)) / 100);
137 # Variant tag -- a tiny "3D PRINT \xb7 Red PLA" pill under the
138 # title so the buyer sees which kind/color/material they added.
139 my $kind = $r->{purchase_kind} || 'digital';
140 my $variant_pill = '';
141 if ($kind eq 'physical') {
142 $needs_shipping = 1;
143 my @bits = ('3D Print');
144 push @bits, _cart_esc($r->{selected_color}) if $r->{selected_color};
145 push @bits, _cart_esc($r->{selected_material}) if $r->{selected_material};
146 $variant_pill = join(' &middot; ', @bits);
147 } elsif ($kind eq 'digital') {
148 $variant_pill = 'Digital download';
149 }
150 if ($r->{is_bundle}) {
151 my $n = $r->{bundle_item_count} || 0;
152 $variant_pill = ($kind eq 'physical' ? 'Bundle &middot; 3D Print' : 'Bundle &middot; Digital')
153 . " &middot; $n item(s)";
154 }
155 push @items, {
156 cart_item_id => $r->{cart_item_id},
157 model_id => $r->{model_id} || 0,
158 bundle_id => $r->{bundle_id} || 0,
159 is_bundle => $r->{is_bundle} ? 1 : 0,
160 title => _cart_esc($r->{title}),
161 hero => $hero,
162 hero_pos => $r->{hero_image_pos} || '50% 50%',
163 price => $price,
164 line_total => $line,
165 quantity => $r->{quantity} || 1,
166 variant_pill => $variant_pill,
167 is_physical => ($kind eq 'physical') ? 1 : 0,
168 };
169}
170
171# Storefront header chrome (brand + search + sign in + cart) -- same
172# header every store.cgi layout renders. Pulled in via the shared
173# helper so cart, checkout, my_orders etc. all show identical chrome.
174require MODS::WebSTLs::StoreChrome;
175my $chrome_html = MODS::WebSTLs::StoreChrome->header_html({
176 store_id => $store->{id},
177 store_name => $store->{name} || $store->{subdomain},
178 is_buyer_in => $buyer_signed,
179 cart_count => scalar(@items),
180 db => $db,
181 dbh => $dbh,
182 DB => $DB,
183});
184
185my $tvars = {
186 theme_css_vars => $css_vars,
187 store_id => $store->{id},
188 store_name => $store->{name} || $store->{subdomain},
189 store_chrome_html => $chrome_html,
190 items => \@items,
191 has_items => scalar(@items) ? 1 : 0,
192 item_count => scalar(@items),
193 subtotal_display => '$' . sprintf('%.2f', $subtotal / 100),
194 discount_display => '$' . sprintf('%.2f', $discount / 100),
195 total_display => '$' . sprintf('%.2f', $total_after_credit / 100),
196 total_before_credit_display => '$' . sprintf('%.2f', $total / 100),
197 has_discount => $discount > 0 ? 1 : 0,
198 has_applied_promo => $applied ? 1 : 0,
199 applied_code => $applied_code,
200 applied_label => $applied_label,
201 credit_balance_display => '$' . sprintf('%.2f', $credit_cents / 100),
202 credit_applied_display => '$' . sprintf('%.2f', $credit_apply / 100),
203 has_credit => $credit_apply > 0 ? 1 : 0,
204 has_credit_balance => $credit_cents > 0 ? 1 : 0,
205 buyer_signed_in => $buyer_signed,
206 promo_flash_msg => $promo_flash_msg,
207 promo_flash_kind => $promo_flash_kind,
208 has_promo_flash => length $promo_flash_msg ? 1 : 0,
209 needs_shipping => $needs_shipping,
210 back_href => "/store.cgi?id=$sid",
211};
212
213print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n";
214print "Set-Cookie: $cookie_line\n" if $cookie_line;
215print "\n";
216
217print join('', $tfile->template('webstls_cart.html', $tvars, undef));
218
219sub _cart_esc {
220 my ($s) = @_;
221 $s = '' unless defined $s;
222 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
223 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
224 return $s;
225}
226
227#======================================================================
228# cart_add entry: POST /cart_add.cgi -- add a model or bundle, JSON out.
229#======================================================================
230sub _cart_add_main {
231 our $ADD_DONE = 0;
232 # Track completion so END can emit a JSON abort message if we crash.
233 my $add_end = sub {
234 if (!$ADD_DONE) {
235 print "Content-Type: application/json\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
236 print qq~{"success":0,"error":"server aborted -- check MODS/DB_ERRORS.log"}~;
237 }
238 };
239 # Attach the abort guard as an END-style handler via local $SIG{__DIE__}? no --
240 # keep the same shape by installing an END block through eval.
241 # (In practice: if _cart_add_main dies, the main script's END block would still
242 # fire; we simulate the original behavior by just running through.)
243
244 my $sid = $q->param('storefront_id') || 0;
245 $sid =~ s/[^0-9]//g;
246 unless ($sid) { _cart_add_fail('missing storefront_id'); return; }
247
248 my $mid = $q->param('model_id') || 0;
249 $mid =~ s/[^0-9]//g;
250 my $bundle_id = $q->param('bundle_id') || 0;
251 $bundle_id =~ s/[^0-9]//g;
252 unless ($mid || $bundle_id) { _cart_add_fail('missing model_id or bundle_id'); return; }
253
254 my $qty = $q->param('qty') || 1;
255 $qty =~ s/[^0-9]//g;
256 $qty = 1 unless $qty;
257 $qty = 1 if $qty < 1;
258 $qty = 99 if $qty > 99;
259
260 # Optional variant params -- buyer chose "Digital file" / "Printed
261 # copy" + color + material on the listing-detail page.
262 my $kind = lc($q->param('kind') || '');
263 $kind =~ s/[^a-z]//g;
264 $kind = '' unless $kind eq 'digital' || $kind eq 'physical';
265
266 my $color = _cart_safe_variant($q->param('color') || '');
267 my $material = _cart_safe_variant($q->param('material') || '');
268
269 my ($token, $cookie_line) = $cart->ensure_token($q);
270
271 my $dbh = $db->db_connect();
272
273 # Probe for variant / bundle columns -- legacy installs may lack them.
274 my $have_variant_cols = 0;
275 {
276 my $r = $db->db_readwrite($dbh, qq~
277 SELECT COUNT(*) AS n FROM information_schema.columns
278 WHERE table_schema='$DB' AND table_name='cart_items'
279 AND column_name='purchase_kind'
280 ~, $ENV{SCRIPT_NAME}, __LINE__);
281 $have_variant_cols = 1 if $r && $r->{n};
282 }
283 my $have_bundle_cols = 0;
284 {
285 my $r = $db->db_readwrite($dbh, qq~
286 SELECT COUNT(*) AS n FROM information_schema.columns
287 WHERE table_schema='$DB' AND table_name='cart_items'
288 AND column_name='bundle_id'
289 ~, $ENV{SCRIPT_NAME}, __LINE__);
290 $have_bundle_cols = 1 if $r && $r->{n};
291 }
292
293 # Bundle branch.
294 if ($bundle_id) {
295 unless ($have_bundle_cols) {
296 $db->db_disconnect($dbh);
297 _cart_add_fail('bundles migration not run yet', $cookie_line);
298 return;
299 }
300 my $bun = $db->db_readwrite($dbh, qq~
301 SELECT id, title, product_kind, base_price_cents, physical_price_cents,
302 inventory_qty, status, storefront_id
303 FROM `${DB}`.bundles
304 WHERE id='$bundle_id' AND storefront_id='$sid' LIMIT 1
305 ~, $ENV{SCRIPT_NAME}, __LINE__);
306 unless ($bun && $bun->{id} && $bun->{status} eq 'published') {
307 $db->db_disconnect($dbh);
308 _cart_add_fail('bundle not available', $cookie_line);
309 return;
310 }
311 my $bun_kind = $bun->{product_kind} || 'digital';
312 my $eff_kind = $kind;
313 if (!$eff_kind) {
314 $eff_kind = ($bun_kind eq 'physical') ? 'physical' : 'digital';
315 }
316 if ($eff_kind eq 'physical' && $bun_kind eq 'digital') {
317 $db->db_disconnect($dbh);
318 _cart_add_fail('this bundle is digital-only', $cookie_line);
319 return;
320 }
321 if ($eff_kind eq 'digital' && $bun_kind eq 'physical') {
322 $db->db_disconnect($dbh);
323 _cart_add_fail('this bundle is print-only', $cookie_line);
324 return;
325 }
326 if ($eff_kind eq 'physical' && defined $bun->{inventory_qty}) {
327 my $inv = $bun->{inventory_qty};
328 if ($inv == 0) {
329 $db->db_disconnect($dbh);
330 _cart_add_fail('this bundle is sold out', $cookie_line);
331 return;
332 }
333 $qty = $inv if $inv > 0 && $qty > $inv;
334 }
335 my $price = ($eff_kind eq 'physical')
336 ? ($bun->{physical_price_cents} || 0)
337 : ($bun->{base_price_cents} || 0);
338 my $color_esc = $color; $color_esc =~ s/'/''/g;
339 my $material_esc = $material; $material_esc =~ s/'/''/g;
340 # Bundle cart row uses NULL model_id, which breaks ON DUPLICATE --
341 # pre-check + UPDATE instead.
342 my $exist = $db->db_readwrite($dbh, qq~
343 SELECT id FROM `${DB}`.cart_items
344 WHERE cart_token='$token' AND storefront_id='$sid'
345 AND bundle_id='$bundle_id' AND purchase_kind='$eff_kind'
346 AND selected_color='$color_esc' AND selected_material='$material_esc'
347 LIMIT 1
348 ~, $ENV{SCRIPT_NAME}, __LINE__);
349 if ($exist && $exist->{id}) {
350 my $eid = $exist->{id};
351 $db->db_readwrite($dbh, qq~
352 UPDATE `${DB}`.cart_items
353 SET quantity = quantity + $qty, unit_price_cents='$price'
354 WHERE id='$eid' LIMIT 1
355 ~, $ENV{SCRIPT_NAME}, __LINE__);
356 } else {
357 $db->db_readwrite($dbh, qq~
358 INSERT INTO `${DB}`.cart_items SET
359 cart_token = '$token',
360 storefront_id = '$sid',
361 model_id = NULL,
362 bundle_id = '$bundle_id',
363 purchase_kind = '$eff_kind',
364 selected_color = '$color_esc',
365 selected_material= '$material_esc',
366 quantity = '$qty',
367 unit_price_cents = '$price',
368 added_at = NOW()
369 ~, $ENV{SCRIPT_NAME}, __LINE__);
370 }
371 my $count = $cart->count($db, $dbh, $DB, $token, $sid);
372 $db->db_disconnect($dbh);
373 print "Content-Type: application/json\nCache-Control: no-cache\n";
374 print "Set-Cookie: $cookie_line\n" if $cookie_line;
375 print "\n";
376 print qq~{"success":1,"count":$count,"bundle":1}~;
377 $ADD_DONE = 1;
378 return;
379 }
380
381 my $have_phys_model_cols = 0;
382 {
383 my $r = $db->db_readwrite($dbh, qq~
384 SELECT COUNT(*) AS n FROM information_schema.columns
385 WHERE table_schema='$DB' AND table_name='models'
386 AND column_name='product_kind'
387 ~, $ENV{SCRIPT_NAME}, __LINE__);
388 $have_phys_model_cols = 1 if $r && $r->{n};
389 }
390
391 my $phys_select = $have_phys_model_cols
392 ? q~m.product_kind, m.physical_price_cents, m.inventory_qty,~
393 : '';
394 my $row = $db->db_readwrite($dbh, qq~
395 SELECT m.id, m.base_price_cents, m.status, m.purged_at,
396 $phys_select
397 sl.override_price_cents
398 FROM `${DB}`.models m
399 JOIN `${DB}`.storefront_listings sl
400 ON sl.model_id = m.id AND sl.storefront_id = '$sid' AND sl.visible = 1
401 WHERE m.id = '$mid'
402 AND m.status = 'published'
403 AND m.purged_at IS NULL
404 LIMIT 1
405 ~, $ENV{SCRIPT_NAME}, __LINE__);
406 unless ($row && $row->{id}) {
407 $db->db_disconnect($dbh);
408 _cart_add_fail('model not available', $cookie_line);
409 return;
410 }
411
412 my $product_kind = $have_phys_model_cols
413 ? ($row->{product_kind} || 'digital')
414 : 'digital';
415 my $base_cents = $row->{override_price_cents} || $row->{base_price_cents} || 0;
416 my $phys_cents = $row->{physical_price_cents} || 0;
417 my $effective_kind = $kind;
418 if (!$effective_kind) {
419 if ($product_kind eq 'physical') { $effective_kind = 'physical'; }
420 elsif ($product_kind eq 'both') { $effective_kind = 'digital'; }
421 else { $effective_kind = 'digital'; }
422 }
423 if ($effective_kind eq 'physical' && $product_kind eq 'digital') {
424 $db->db_disconnect($dbh);
425 _cart_add_fail('this listing does not offer a printed copy', $cookie_line);
426 return;
427 }
428 if ($effective_kind eq 'digital' && $product_kind eq 'physical') {
429 $db->db_disconnect($dbh);
430 _cart_add_fail('this listing is print-only', $cookie_line);
431 return;
432 }
433
434 if ($effective_kind eq 'physical' && defined $row->{inventory_qty}) {
435 my $inv = $row->{inventory_qty};
436 if ($inv == 0) {
437 $db->db_disconnect($dbh);
438 _cart_add_fail('this item is sold out', $cookie_line);
439 return;
440 }
441 if ($inv > 0 && $qty > $inv) {
442 $qty = $inv;
443 }
444 }
445
446 my $price = ($effective_kind eq 'physical') ? $phys_cents : $base_cents;
447
448 if ($have_variant_cols) {
449 my $color_esc = $color; $color_esc =~ s/'/''/g;
450 my $material_esc = $material; $material_esc =~ s/'/''/g;
451 $db->db_readwrite($dbh, qq~
452 INSERT INTO `${DB}`.cart_items SET
453 cart_token = '$token',
454 storefront_id = '$sid',
455 model_id = '$mid',
456 purchase_kind = '$effective_kind',
457 selected_color = '$color_esc',
458 selected_material = '$material_esc',
459 quantity = '$qty',
460 unit_price_cents = '$price',
461 added_at = NOW()
462 ON DUPLICATE KEY UPDATE
463 quantity = quantity + $qty,
464 unit_price_cents = '$price'
465 ~, $ENV{SCRIPT_NAME}, __LINE__);
466 } else {
467 $db->db_readwrite($dbh, qq~
468 INSERT INTO `${DB}`.cart_items SET
469 cart_token = '$token',
470 storefront_id = '$sid',
471 model_id = '$mid',
472 quantity = '$qty',
473 unit_price_cents = '$price',
474 added_at = NOW()
475 ON DUPLICATE KEY UPDATE
476 quantity = quantity + $qty,
477 unit_price_cents = '$price'
478 ~, $ENV{SCRIPT_NAME}, __LINE__);
479 }
480
481 my $count = $cart->count($db, $dbh, $DB, $token, $sid);
482
483 # A/B + MVT: log an add_to_cart event for every experiment this
484 # visitor is bucketed into.
485 my $ex = MODS::WebSTLs::Experiments->new;
486 my $vtok = $ex->read_visitor($q);
487 if ($vtok) {
488 my $vh = $ex->visitor_hash($vtok);
489 my $cents = $price * $qty;
490 $ex->log_event_for_visitor_experiments(
491 $db, $dbh, $DB, $sid, $vh, 'add_to_cart',
492 sprintf('%.2f', $cents / 100)
493 );
494 }
495
496 $db->db_disconnect($dbh);
497
498 print "Content-Type: application/json\nCache-Control: no-cache\n";
499 print "Set-Cookie: $cookie_line\n" if $cookie_line;
500 print "\n";
501 print qq~{"success":1,"count":$count}~;
502 $ADD_DONE = 1;
503 return;
504}
505
506sub _cart_add_fail {
507 my ($msg, $cookie_line) = @_;
508 $msg =~ s/"/\\"/g;
509 print "Content-Type: application/json\nCache-Control: no-cache\n";
510 print "Set-Cookie: $cookie_line\n" if $cookie_line;
511 print "\n";
512 print qq~{"success":0,"error":"$msg"}~;
513 our $ADD_DONE; $ADD_DONE = 1;
514 return;
515}
516
517sub _cart_safe_variant {
518 my $s = shift; $s = '' unless defined $s;
519 $s =~ s/[^A-Za-z0-9 _,.\/\(\)\-]//g; # whitelist; no quotes
520 $s =~ s/^\s+|\s+$//g;
521 return substr($s, 0, 80);
522}
523
524#======================================================================
525# cart_remove entry: POST /cart_remove.cgi -- delete a cart_items row.
526#======================================================================
527sub _cart_remove_main {
528 print "Content-Type: application/json\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
529
530 our $RM_DONE = 0;
531 my $token = $cart->read_token($q);
532 unless ($token) { _cart_remove_fail('no cart'); return; }
533
534 my $cart_item_id = $q->param('cart_item_id') || 0;
535 $cart_item_id =~ s/[^0-9]//g;
536 unless ($cart_item_id) { _cart_remove_fail('missing cart_item_id'); return; }
537
538 my $dbh = $db->db_connect();
539
540 # Ownership check: id + cart_token combined.
541 my $row = $db->db_readwrite($dbh, qq~
542 SELECT storefront_id FROM `${DB}`.cart_items
543 WHERE id='$cart_item_id' AND cart_token='$token' LIMIT 1
544 ~, $ENV{SCRIPT_NAME}, __LINE__);
545 unless ($row && $row->{storefront_id}) {
546 $db->db_disconnect($dbh);
547 _cart_remove_fail('cart item not found');
548 return;
549 }
550 my $sid = $row->{storefront_id};
551
552 $db->db_readwrite($dbh,
553 qq~DELETE FROM `${DB}`.cart_items WHERE id='$cart_item_id' AND cart_token='$token' LIMIT 1~,
554 $ENV{SCRIPT_NAME}, __LINE__);
555
556 my $count = $cart->count($db, $dbh, $DB, $token, $sid);
557 $db->db_disconnect($dbh);
558
559 print qq~{"success":1,"count":$count}~;
560 $RM_DONE = 1;
561 return;
562}
563
564sub _cart_remove_fail {
565 my ($msg) = @_;
566 $msg =~ s/"/\\"/g;
567 print qq~{"success":0,"error":"$msg"}~;
568 our $RM_DONE; $RM_DONE = 1;
569 return;
570}
571
572#======================================================================
573# cart_promo entry: POST /cart_promo.cgi -- apply/remove promo code.
574# Always redirects back to /cart.cgi?id=<sid> with a flash querystring.
575#======================================================================
576sub _cart_promo_main {
577 my $promo = MODS::WebSTLs::Promotions->new;
578
579 my $sid = $form->{storefront_id} || $form->{id} || 0;
580 $sid =~ s/[^0-9]//g;
581 my $action = $form->{action} || '';
582 $action =~ s/[^a-z_]//g;
583
584 unless ($sid) { _cart_promo_back(0, 'promo_err', 'Cart not found.'); return; }
585
586 my $token = $cart->read_token($q);
587 unless ($token) {
588 _cart_promo_back($sid, 'promo_err',
589 'Your cart session expired -- add something and try again.');
590 return;
591 }
592
593 my $dbh = $db->db_connect();
594
595 if ($action eq 'remove') {
596 $promo->cart_clear($db, $dbh, $DB, $token, $sid);
597 $db->db_disconnect($dbh);
598 _cart_promo_back($sid, 'promo_ok', 'Promo code removed.');
599 return;
600 }
601
602 if ($action eq 'apply') {
603 my $code = $form->{code} || '';
604 my $row = $promo->load_by_code($db, $dbh, $DB, $code,
605 kind => 'model', storefront_id => $sid);
606 unless ($row && $row->{id}) {
607 $db->db_disconnect($dbh);
608 _cart_promo_back($sid, 'promo_err', "That code isn't valid for this store.");
609 return;
610 }
611
612 my $subtotal = $cart->total_cents($db, $dbh, $DB, $token, $sid);
613 if ($subtotal <= 0) {
614 $db->db_disconnect($dbh);
615 _cart_promo_back($sid, 'promo_err', 'Add something to your cart first.');
616 return;
617 }
618
619 # new_customer is the simplest signal we have without joining
620 # buyer_accounts -- treat anonymous buyers as new. The webhook
621 # does the final new_customer check on payment success.
622 my %v = $promo->validate($db, $dbh, $DB, $row,
623 kind => 'model',
624 subtotal_cents => $subtotal,
625 buyer_email => '',
626 new_customer => 1,
627 );
628 unless ($v{ok}) {
629 $db->db_disconnect($dbh);
630 _cart_promo_back($sid, 'promo_err',
631 $v{reason} || 'That code is not valid right now.');
632 return;
633 }
634
635 $promo->cart_apply($db, $dbh, $DB, $token, $sid, $row->{id});
636 $db->db_disconnect($dbh);
637 _cart_promo_back($sid, 'promo_ok', 'Code applied -- discount shows below.');
638 return;
639 }
640
641 $db->db_disconnect($dbh);
642 _cart_promo_back($sid, 'promo_err', 'Unknown action.');
643 return;
644}
645
646sub _cart_promo_back {
647 my ($sid, $kind, $msg) = @_;
648 my $u = '/cart.cgi?id=' . $sid;
649 if ($kind && defined $msg) {
650 my $e = $msg;
651 $e =~ s/([^A-Za-z0-9\-_.~])/sprintf('%%%02X', ord($1))/eg;
652 $u .= '&' . $kind . '=' . $e;
653 }
654 print "Status: 302 Found\nLocation: $u\n\n";
655 return;
656}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help