Diff -- /var/www/vhosts/3dshawn.com/shop.3dshawn.com/cart.cgi
Diff

/var/www/vhosts/3dshawn.com/shop.3dshawn.com/cart.cgi

added on local at 2026-07-01 22:09:30

Added
+593
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to c9df9a187782
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# ShopCart -- view cart for one storefront + associated cart ops.
4#
5# Consolidates the former quartet via SCRIPT_NAME dispatch:
6# cart.cgi -- HTML view of the cart (primary)
7# cart_add.cgi -- POST: add a model/bundle to the cart -> JSON
8# cart_remove.cgi -- POST: delete a cart row -> JSON
9# cart_promo.cgi -- POST: apply/remove a promo code -> 302 back
10#======================================================================
11use strict;
12use warnings;
13
14use lib '/var/www/vhosts/3dshawn.com/shop.3dshawn.com';
15use CGI;
16use MODS::Template;
17use MODS::DBConnect;
18use MODS::ShopCart::Config;
19use MODS::ShopCart::Themes;
20use MODS::ShopCart::Cart;
21use MODS::ShopCart::Promotions;
22use MODS::ShopCart::BuyerAuth;
23use MODS::ShopCart::BuyerCredits;
24use MODS::ShopCart::Experiments;
25
26$| = 1;
27
28my $q = CGI->new;
29my $form = $q->Vars;
30my $tfile = MODS::Template->new;
31my $db = MODS::DBConnect->new;
32my $config = MODS::ShopCart::Config->new;
33my $themes = MODS::ShopCart::Themes->new;
34my $cart = MODS::ShopCart::Cart->new;
35my $DB = $config->settings('database_name');
36
37my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'cart';
38
39if ($entry eq 'cart_add') { _cart_add(); exit; }
40elsif ($entry eq 'cart_remove') { _cart_remove(); exit; }
41elsif ($entry eq 'cart_promo') { _cart_promo(); exit; }
42else { _cart_view(); exit; }
43
44#======================================================================
45# cart.cgi -- HTML view
46#======================================================================
47sub _cart_view {
48 my $sid = $form->{id} || $form->{storefront_id} || 0;
49 $sid =~ s/[^0-9]//g;
50 unless ($sid) {
51 print "Status: 302 Found\nLocation: /\n\n";
52 return;
53 }
54
55 my $dbh = $db->db_connect();
56
57 my $store = $db->db_readwrite($dbh,
58 qq~SELECT id, name, subdomain, theme_id FROM `${DB}`.storefronts WHERE id='$sid' AND status='live' LIMIT 1~,
59 $ENV{SCRIPT_NAME}, __LINE__);
60 unless ($store && $store->{id}) {
61 $db->db_disconnect($dbh);
62 print "Status: 302 Found\nLocation: /\n\n";
63 return;
64 }
65
66 my $theme = $themes->by_id($store->{theme_id});
67 my $css_vars = $themes->css_vars($theme);
68
69 my ($token, $cookie_line) = $cart->ensure_token($q);
70 my @rows = $cart->items($db, $dbh, $DB, $token, $sid);
71 my $subtotal = $cart->total_cents($db, $dbh, $DB, $token, $sid);
72
73 my $promo = MODS::ShopCart::Promotions->new;
74 my $applied = $promo->cart_applied($db, $dbh, $DB, $token, $sid);
75 my $discount = $applied ? $promo->apply_to_total_cents($applied, $subtotal) : 0;
76 my $total = $subtotal - $discount;
77 $total = 0 if $total < 0;
78
79 my $buyer_auth = MODS::ShopCart::BuyerAuth->new;
80 my $bc = MODS::ShopCart::BuyerCredits->new;
81 my $credit_cents = 0;
82 my $credit_apply = 0;
83 my $buyer_signed = 0;
84 {
85 my $b = $buyer_auth->verify($q, $db, $dbh, $DB);
86 if ($b && $b->{id} && $b->{storefront_id} && $b->{storefront_id} eq $sid) {
87 $buyer_signed = 1;
88 $credit_cents = $bc->balance_cents($db, $dbh, $DB, $sid,
89 { buyer_account_id => $b->{id} });
90 if ($credit_cents > 0 && $total > 0) {
91 $credit_apply = $credit_cents > $total ? $total : $credit_cents;
92 }
93 }
94 }
95 my $total_after_credit = $total - $credit_apply;
96 $total_after_credit = 0 if $total_after_credit < 0;
97
98 $db->db_disconnect($dbh);
99
100 my $applied_code = $applied ? ($applied->{code} || '') : '';
101 my $applied_label = $applied ? $promo->marketing_label($applied) : '';
102
103 my $promo_flash_msg = '';
104 my $promo_flash_kind = '';
105 if (defined $form->{promo_ok}) {
106 $promo_flash_kind = 'ok';
107 $promo_flash_msg = $form->{promo_ok};
108 } elsif (defined $form->{promo_err}) {
109 $promo_flash_kind = 'err';
110 $promo_flash_msg = $form->{promo_err};
111 }
112 $promo_flash_msg =~ s/[^\x20-\x7E]//g;
113 $promo_flash_msg = substr($promo_flash_msg, 0, 200);
114
115 my @items;
116 my $needs_shipping = 0;
117 foreach my $r (@rows) {
118 my $hero = $r->{hero_image_url} || '/assets/no-cover.svg';
119 my $price = '$' . sprintf('%.2f', ($r->{unit_price_cents} || 0) / 100);
120 my $line = '$' . sprintf('%.2f', (($r->{unit_price_cents} || 0) * ($r->{quantity} || 1)) / 100);
121 my $kind = $r->{purchase_kind} || 'digital';
122 my $variant_pill = '';
123 if ($kind eq 'physical') {
124 $needs_shipping = 1;
125 my @bits = ('3D Print');
126 push @bits, _cart_h($r->{selected_color}) if $r->{selected_color};
127 push @bits, _cart_h($r->{selected_material}) if $r->{selected_material};
128 $variant_pill = join(' &middot; ', @bits);
129 } elsif ($kind eq 'digital') {
130 $variant_pill = 'Digital download';
131 }
132 if ($r->{is_bundle}) {
133 my $n = $r->{bundle_item_count} || 0;
134 $variant_pill = ($kind eq 'physical' ? 'Bundle &middot; 3D Print' : 'Bundle &middot; Digital')
135 . " &middot; $n item(s)";
136 }
137 push @items, {
138 cart_item_id => $r->{cart_item_id},
139 model_id => $r->{model_id} || 0,
140 bundle_id => $r->{bundle_id} || 0,
141 is_bundle => $r->{is_bundle} ? 1 : 0,
142 title => _cart_h($r->{title}),
143 hero => $hero,
144 hero_pos => $r->{hero_image_pos} || '50% 50%',
145 price => $price,
146 line_total => $line,
147 quantity => $r->{quantity} || 1,
148 variant_pill => $variant_pill,
149 is_physical => ($kind eq 'physical') ? 1 : 0,
150 };
151 }
152
153 require MODS::ShopCart::StoreChrome;
154 my $chrome_html = MODS::ShopCart::StoreChrome->header_html({
155 store_id => $store->{id},
156 store_name => $store->{name} || $store->{subdomain},
157 is_buyer_in => $buyer_signed,
158 cart_count => scalar(@items),
159 db => $db,
160 dbh => $dbh,
161 DB => $DB,
162 });
163
164 my $tvars = {
165 theme_css_vars => $css_vars,
166 store_id => $store->{id},
167 store_name => $store->{name} || $store->{subdomain},
168 store_chrome_html => $chrome_html,
169 items => \@items,
170 has_items => scalar(@items) ? 1 : 0,
171 item_count => scalar(@items),
172 subtotal_display => '$' . sprintf('%.2f', $subtotal / 100),
173 discount_display => '$' . sprintf('%.2f', $discount / 100),
174 total_display => '$' . sprintf('%.2f', $total_after_credit / 100),
175 total_before_credit_display => '$' . sprintf('%.2f', $total / 100),
176 has_discount => $discount > 0 ? 1 : 0,
177 has_applied_promo => $applied ? 1 : 0,
178 applied_code => $applied_code,
179 applied_label => $applied_label,
180 credit_balance_display => '$' . sprintf('%.2f', $credit_cents / 100),
181 credit_applied_display => '$' . sprintf('%.2f', $credit_apply / 100),
182 has_credit => $credit_apply > 0 ? 1 : 0,
183 has_credit_balance => $credit_cents > 0 ? 1 : 0,
184 buyer_signed_in => $buyer_signed,
185 promo_flash_msg => $promo_flash_msg,
186 promo_flash_kind => $promo_flash_kind,
187 has_promo_flash => length $promo_flash_msg ? 1 : 0,
188 needs_shipping => $needs_shipping,
189 back_href => "/store.cgi?id=$sid",
190 };
191
192 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n";
193 print "Set-Cookie: $cookie_line\n" if $cookie_line;
194 print "\n";
195
196 print join('', $tfile->template('shopcart_cart.html', $tvars, undef));
197 return;
198}
199
200#======================================================================
201# cart_add.cgi -- add model/bundle to cart, return JSON
202#======================================================================
203sub _cart_add {
204 our $ADD_DONE = 0;
205 my $done_ref = \$ADD_DONE;
206
207 my $add_fail = sub {
208 my ($msg, $cookie_line) = @_;
209 $msg =~ s/"/\\"/g;
210 print "Content-Type: application/json\nCache-Control: no-cache\n";
211 print "Set-Cookie: $cookie_line\n" if $cookie_line;
212 print "\n";
213 print qq~{"success":0,"error":"$msg"}~;
214 $$done_ref = 1;
215 exit;
216 };
217
218 my $sid = $q->param('storefront_id') || 0;
219 $sid =~ s/[^0-9]//g;
220 $add_fail->('missing storefront_id') unless $sid;
221
222 my $mid = $q->param('model_id') || 0;
223 $mid =~ s/[^0-9]//g;
224 my $bundle_id = $q->param('bundle_id') || 0;
225 $bundle_id =~ s/[^0-9]//g;
226 $add_fail->('missing model_id or bundle_id') unless $mid || $bundle_id;
227
228 my $qty = $q->param('qty') || 1;
229 $qty =~ s/[^0-9]//g;
230 $qty = 1 unless $qty;
231 $qty = 1 if $qty < 1;
232 $qty = 99 if $qty > 99;
233
234 my $kind = lc($q->param('kind') || '');
235 $kind =~ s/[^a-z]//g;
236 $kind = '' unless $kind eq 'digital' || $kind eq 'physical';
237
238 my $color = _cart_safe_variant($q->param('color') || '');
239 my $material = _cart_safe_variant($q->param('material') || '');
240
241 my ($token, $cookie_line) = $cart->ensure_token($q);
242
243 my $dbh = $db->db_connect();
244
245 my $have_variant_cols = 0;
246 {
247 my $r = $db->db_readwrite($dbh, qq~
248 SELECT COUNT(*) AS n FROM information_schema.columns
249 WHERE table_schema='$DB' AND table_name='cart_items'
250 AND column_name='purchase_kind'
251 ~, $ENV{SCRIPT_NAME}, __LINE__);
252 $have_variant_cols = 1 if $r && $r->{n};
253 }
254 my $have_bundle_cols = 0;
255 {
256 my $r = $db->db_readwrite($dbh, qq~
257 SELECT COUNT(*) AS n FROM information_schema.columns
258 WHERE table_schema='$DB' AND table_name='cart_items'
259 AND column_name='bundle_id'
260 ~, $ENV{SCRIPT_NAME}, __LINE__);
261 $have_bundle_cols = 1 if $r && $r->{n};
262 }
263
264 if ($bundle_id) {
265 unless ($have_bundle_cols) {
266 $db->db_disconnect($dbh);
267 $add_fail->('bundles migration not run yet', $cookie_line);
268 }
269 my $bun = $db->db_readwrite($dbh, qq~
270 SELECT id, title, product_kind, base_price_cents, physical_price_cents,
271 inventory_qty, status, storefront_id
272 FROM `${DB}`.bundles
273 WHERE id='$bundle_id' AND storefront_id='$sid' LIMIT 1
274 ~, $ENV{SCRIPT_NAME}, __LINE__);
275 unless ($bun && $bun->{id} && $bun->{status} eq 'published') {
276 $db->db_disconnect($dbh);
277 $add_fail->('bundle not available', $cookie_line);
278 }
279 my $bun_kind = $bun->{product_kind} || 'digital';
280 my $eff_kind = $kind;
281 if (!$eff_kind) {
282 $eff_kind = ($bun_kind eq 'physical') ? 'physical' : 'digital';
283 }
284 if ($eff_kind eq 'physical' && $bun_kind eq 'digital') {
285 $db->db_disconnect($dbh);
286 $add_fail->('this bundle is digital-only', $cookie_line);
287 }
288 if ($eff_kind eq 'digital' && $bun_kind eq 'physical') {
289 $db->db_disconnect($dbh);
290 $add_fail->('this bundle is print-only', $cookie_line);
291 }
292 if ($eff_kind eq 'physical' && defined $bun->{inventory_qty}) {
293 my $inv = $bun->{inventory_qty};
294 if ($inv == 0) {
295 $db->db_disconnect($dbh);
296 $add_fail->('this bundle is sold out', $cookie_line);
297 }
298 $qty = $inv if $inv > 0 && $qty > $inv;
299 }
300 my $price = ($eff_kind eq 'physical')
301 ? ($bun->{physical_price_cents} || 0)
302 : ($bun->{base_price_cents} || 0);
303 my $color_esc = $color; $color_esc =~ s/'/''/g;
304 my $material_esc = $material; $material_esc =~ s/'/''/g;
305 my $exist = $db->db_readwrite($dbh, qq~
306 SELECT id FROM `${DB}`.cart_items
307 WHERE cart_token='$token' AND storefront_id='$sid'
308 AND bundle_id='$bundle_id' AND purchase_kind='$eff_kind'
309 AND selected_color='$color_esc' AND selected_material='$material_esc'
310 LIMIT 1
311 ~, $ENV{SCRIPT_NAME}, __LINE__);
312 if ($exist && $exist->{id}) {
313 my $eid = $exist->{id};
314 $db->db_readwrite($dbh, qq~
315 UPDATE `${DB}`.cart_items
316 SET quantity = quantity + $qty, unit_price_cents='$price'
317 WHERE id='$eid' LIMIT 1
318 ~, $ENV{SCRIPT_NAME}, __LINE__);
319 } else {
320 $db->db_readwrite($dbh, qq~
321 INSERT INTO `${DB}`.cart_items SET
322 cart_token = '$token',
323 storefront_id = '$sid',
324 model_id = NULL,
325 bundle_id = '$bundle_id',
326 purchase_kind = '$eff_kind',
327 selected_color = '$color_esc',
328 selected_material= '$material_esc',
329 quantity = '$qty',
330 unit_price_cents = '$price',
331 added_at = NOW()
332 ~, $ENV{SCRIPT_NAME}, __LINE__);
333 }
334 my $count = $cart->count($db, $dbh, $DB, $token, $sid);
335 $db->db_disconnect($dbh);
336 print "Content-Type: application/json\nCache-Control: no-cache\n";
337 print "Set-Cookie: $cookie_line\n" if $cookie_line;
338 print "\n";
339 print qq~{"success":1,"count":$count,"bundle":1}~;
340 $ADD_DONE = 1;
341 return;
342 }
343
344 my $have_phys_model_cols = 0;
345 {
346 my $r = $db->db_readwrite($dbh, qq~
347 SELECT COUNT(*) AS n FROM information_schema.columns
348 WHERE table_schema='$DB' AND table_name='models'
349 AND column_name='product_kind'
350 ~, $ENV{SCRIPT_NAME}, __LINE__);
351 $have_phys_model_cols = 1 if $r && $r->{n};
352 }
353
354 my $phys_select = $have_phys_model_cols
355 ? q~m.product_kind, m.physical_price_cents, m.inventory_qty,~
356 : '';
357 my $row = $db->db_readwrite($dbh, qq~
358 SELECT m.id, m.base_price_cents, m.status, m.purged_at,
359 $phys_select
360 sl.override_price_cents
361 FROM `${DB}`.models m
362 JOIN `${DB}`.storefront_listings sl
363 ON sl.model_id = m.id AND sl.storefront_id = '$sid' AND sl.visible = 1
364 WHERE m.id = '$mid'
365 AND m.status = 'published'
366 AND m.purged_at IS NULL
367 LIMIT 1
368 ~, $ENV{SCRIPT_NAME}, __LINE__);
369 unless ($row && $row->{id}) {
370 $db->db_disconnect($dbh);
371 $add_fail->('model not available', $cookie_line);
372 }
373
374 my $product_kind = $have_phys_model_cols
375 ? ($row->{product_kind} || 'digital')
376 : 'digital';
377 my $base_cents = $row->{override_price_cents} || $row->{base_price_cents} || 0;
378 my $phys_cents = $row->{physical_price_cents} || 0;
379 my $effective_kind = $kind;
380 if (!$effective_kind) {
381 if ($product_kind eq 'physical') { $effective_kind = 'physical'; }
382 elsif ($product_kind eq 'both') { $effective_kind = 'digital'; }
383 else { $effective_kind = 'digital'; }
384 }
385 if ($effective_kind eq 'physical' && $product_kind eq 'digital') {
386 $db->db_disconnect($dbh);
387 $add_fail->('this listing does not offer a printed copy', $cookie_line);
388 }
389 if ($effective_kind eq 'digital' && $product_kind eq 'physical') {
390 $db->db_disconnect($dbh);
391 $add_fail->('this listing is print-only', $cookie_line);
392 }
393
394 if ($effective_kind eq 'physical' && defined $row->{inventory_qty}) {
395 my $inv = $row->{inventory_qty};
396 if ($inv == 0) {
397 $db->db_disconnect($dbh);
398 $add_fail->('this item is sold out', $cookie_line);
399 }
400 if ($inv > 0 && $qty > $inv) {
401 $qty = $inv;
402 }
403 }
404
405 my $price = ($effective_kind eq 'physical') ? $phys_cents : $base_cents;
406
407 if ($have_variant_cols) {
408 my $color_esc = $color; $color_esc =~ s/'/''/g;
409 my $material_esc = $material; $material_esc =~ s/'/''/g;
410 $db->db_readwrite($dbh, qq~
411 INSERT INTO `${DB}`.cart_items SET
412 cart_token = '$token',
413 storefront_id = '$sid',
414 model_id = '$mid',
415 purchase_kind = '$effective_kind',
416 selected_color = '$color_esc',
417 selected_material = '$material_esc',
418 quantity = '$qty',
419 unit_price_cents = '$price',
420 added_at = NOW()
421 ON DUPLICATE KEY UPDATE
422 quantity = quantity + $qty,
423 unit_price_cents = '$price'
424 ~, $ENV{SCRIPT_NAME}, __LINE__);
425 } else {
426 $db->db_readwrite($dbh, qq~
427 INSERT INTO `${DB}`.cart_items SET
428 cart_token = '$token',
429 storefront_id = '$sid',
430 model_id = '$mid',
431 quantity = '$qty',
432 unit_price_cents = '$price',
433 added_at = NOW()
434 ON DUPLICATE KEY UPDATE
435 quantity = quantity + $qty,
436 unit_price_cents = '$price'
437 ~, $ENV{SCRIPT_NAME}, __LINE__);
438 }
439
440 my $count = $cart->count($db, $dbh, $DB, $token, $sid);
441
442 my $ex = MODS::ShopCart::Experiments->new;
443 my $vtok = $ex->read_visitor($q);
444 if ($vtok) {
445 my $vh = $ex->visitor_hash($vtok);
446 my $cents = $price * $qty;
447 $ex->log_event_for_visitor_experiments(
448 $db, $dbh, $DB, $sid, $vh, 'add_to_cart',
449 sprintf('%.2f', $cents / 100)
450 );
451 }
452
453 $db->db_disconnect($dbh);
454
455 print "Content-Type: application/json\nCache-Control: no-cache\n";
456 print "Set-Cookie: $cookie_line\n" if $cookie_line;
457 print "\n";
458 print qq~{"success":1,"count":$count}~;
459 $ADD_DONE = 1;
460 return;
461}
462
463sub _cart_safe_variant {
464 my $s = shift; $s = '' unless defined $s;
465 $s =~ s/[^A-Za-z0-9 _,.\/\(\)\-]//g;
466 $s =~ s/^\s+|\s+$//g;
467 return substr($s, 0, 80);
468}
469
470#======================================================================
471# cart_remove.cgi -- delete a cart row, return JSON
472#======================================================================
473sub _cart_remove {
474 print "Content-Type: application/json\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
475
476 our $REM_DONE = 0;
477
478 my $rem_fail = sub {
479 my ($msg) = @_;
480 $msg =~ s/"/\\"/g;
481 print qq~{"success":0,"error":"$msg"}~;
482 $REM_DONE = 1;
483 exit;
484 };
485
486 my $token = $cart->read_token($q);
487 $rem_fail->('no cart') unless $token;
488
489 my $cart_item_id = $q->param('cart_item_id') || 0;
490 $cart_item_id =~ s/[^0-9]//g;
491 $rem_fail->('missing cart_item_id') unless $cart_item_id;
492
493 my $dbh = $db->db_connect();
494
495 my $row = $db->db_readwrite($dbh, qq~
496 SELECT storefront_id FROM `${DB}`.cart_items
497 WHERE id='$cart_item_id' AND cart_token='$token' LIMIT 1
498 ~, $ENV{SCRIPT_NAME}, __LINE__);
499 unless ($row && $row->{storefront_id}) {
500 $db->db_disconnect($dbh);
501 $rem_fail->('cart item not found');
502 }
503 my $sid = $row->{storefront_id};
504
505 $db->db_readwrite($dbh,
506 qq~DELETE FROM `${DB}`.cart_items WHERE id='$cart_item_id' AND cart_token='$token' LIMIT 1~,
507 $ENV{SCRIPT_NAME}, __LINE__);
508
509 my $count = $cart->count($db, $dbh, $DB, $token, $sid);
510 $db->db_disconnect($dbh);
511
512 print qq~{"success":1,"count":$count}~;
513 $REM_DONE = 1;
514 return;
515}
516
517#======================================================================
518# cart_promo.cgi -- apply/remove promo, redirect back to /cart.cgi
519#======================================================================
520sub _cart_promo {
521 my $promo = MODS::ShopCart::Promotions->new;
522 my $sid = $form->{storefront_id} || $form->{id} || 0;
523 $sid =~ s/[^0-9]//g;
524 my $action = $form->{action} || '';
525 $action =~ s/[^a-z_]//g;
526
527 my $back = sub {
528 my ($sid, $kind, $msg) = @_;
529 my $u = '/cart.cgi?id=' . $sid;
530 if ($kind && defined $msg) {
531 my $e = $msg;
532 $e =~ s/([^A-Za-z0-9\-_.~])/sprintf('%%%02X', ord($1))/eg;
533 $u .= '&' . $kind . '=' . $e;
534 }
535 print "Status: 302 Found\nLocation: $u\n\n";
536 exit;
537 };
538
539 $back->($sid, 'promo_err', 'Cart not found.') unless $sid;
540 my $token = $cart->read_token($q);
541 $back->($sid, 'promo_err', 'Your cart session expired -- add something and try again.') unless $token;
542
543 my $dbh = $db->db_connect();
544
545 if ($action eq 'remove') {
546 $promo->cart_clear($db, $dbh, $DB, $token, $sid);
547 $db->db_disconnect($dbh);
548 $back->($sid, 'promo_ok', 'Promo code removed.');
549 }
550
551 if ($action eq 'apply') {
552 my $code = $form->{code} || '';
553 my $row = $promo->load_by_code($db, $dbh, $DB, $code,
554 kind => 'model', storefront_id => $sid);
555 unless ($row && $row->{id}) {
556 $db->db_disconnect($dbh);
557 $back->($sid, 'promo_err', "That code isn't valid for this store.");
558 }
559
560 my $subtotal = $cart->total_cents($db, $dbh, $DB, $token, $sid);
561 if ($subtotal <= 0) {
562 $db->db_disconnect($dbh);
563 $back->($sid, 'promo_err', 'Add something to your cart first.');
564 }
565
566 my %v = $promo->validate($db, $dbh, $DB, $row,
567 kind => 'model',
568 subtotal_cents => $subtotal,
569 buyer_email => '',
570 new_customer => 1,
571 );
572 unless ($v{ok}) {
573 $db->db_disconnect($dbh);
574 $back->($sid, 'promo_err', $v{reason} || 'That code is not valid right now.');
575 }
576
577 $promo->cart_apply($db, $dbh, $DB, $token, $sid, $row->{id});
578 $db->db_disconnect($dbh);
579 $back->($sid, 'promo_ok', 'Code applied -- discount shows below.');
580 }
581
582 $db->db_disconnect($dbh);
583 $back->($sid, 'promo_err', 'Unknown action.');
584 return;
585}
586
587sub _cart_h {
588 my ($s) = @_;
589 $s = '' unless defined $s;
590 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
591 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
592 return $s;
593}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help