added on local at 2026-07-01 21:47:31
| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # RePricer -- view cart for one storefront. | |
| 4 | # | |
| 5 | # GET /cart.cgi?id=<storefront_id> | |
| 6 | # | |
| 7 | # Renders the cart contents using the storefront's theme so it feels | |
| 8 | # like a continuation of the shopping experience. Each row has a | |
| 9 | # Remove (X) button. A Checkout button at the bottom kicks off the | |
| 10 | # Stripe Elements payment flow (see /checkout.cgi). | |
| 11 | #====================================================================== | |
| 12 | use strict; | |
| 13 | use warnings; | |
| 14 | ||
| 15 | use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com'; | |
| 16 | use CGI; | |
| 17 | use MODS::Template; | |
| 18 | use MODS::DBConnect; | |
| 19 | use MODS::RePricer::Config; | |
| 20 | use MODS::RePricer::Themes; | |
| 21 | use MODS::RePricer::Cart; | |
| 22 | use MODS::RePricer::Promotions; | |
| 23 | use MODS::RePricer::BuyerAuth; | |
| 24 | use MODS::RePricer::BuyerCredits; | |
| 25 | ||
| 26 | $| = 1; | |
| 27 | ||
| 28 | my $q = CGI->new; | |
| 29 | my $form = $q->Vars; | |
| 30 | my $tfile = MODS::Template->new; | |
| 31 | my $db = MODS::DBConnect->new; | |
| 32 | my $config = MODS::RePricer::Config->new; | |
| 33 | my $themes = MODS::RePricer::Themes->new; | |
| 34 | my $cart = MODS::RePricer::Cart->new; | |
| 35 | my $DB = $config->settings('database_name'); | |
| 36 | ||
| 37 | my $sid = $form->{id} || $form->{storefront_id} || 0; | |
| 38 | $sid =~ s/[^0-9]//g; | |
| 39 | unless ($sid) { | |
| 40 | print "Status: 302 Found\nLocation: /\n\n"; | |
| 41 | exit; | |
| 42 | } | |
| 43 | ||
| 44 | my $dbh = $db->db_connect(); | |
| 45 | ||
| 46 | # Pull the storefront so the cart page can render in its theme. | |
| 47 | my $store = $db->db_readwrite($dbh, | |
| 48 | qq~SELECT id, name, subdomain, theme_id FROM `${DB}`.storefronts WHERE id='$sid' AND status='live' LIMIT 1~, | |
| 49 | $ENV{SCRIPT_NAME}, __LINE__); | |
| 50 | unless ($store && $store->{id}) { | |
| 51 | $db->db_disconnect($dbh); | |
| 52 | print "Status: 302 Found\nLocation: /\n\n"; | |
| 53 | exit; | |
| 54 | } | |
| 55 | ||
| 56 | my $theme = $themes->by_id($store->{theme_id}); | |
| 57 | my $css_vars = $themes->css_vars($theme); | |
| 58 | ||
| 59 | my ($token, $cookie_line) = $cart->ensure_token($q); | |
| 60 | my @rows = $cart->items($db, $dbh, $DB, $token, $sid); | |
| 61 | my $subtotal = $cart->total_cents($db, $dbh, $DB, $token, $sid); | |
| 62 | ||
| 63 | # Promo state: an applied cart-promo lives in cart_promotions; we also | |
| 64 | # surface the platform's currently-marquee'd 'model' promo (if any) so | |
| 65 | # buyers see what's on offer before they even type a code. | |
| 66 | my $promo = MODS::RePricer::Promotions->new; | |
| 67 | my $applied = $promo->cart_applied($db, $dbh, $DB, $token, $sid); | |
| 68 | my $discount = $applied ? $promo->apply_to_total_cents($applied, $subtotal) : 0; | |
| 69 | my $total = $subtotal - $discount; | |
| 70 | $total = 0 if $total < 0; | |
| 71 | ||
| 72 | # Store credit available to this buyer (registered only -- guests can | |
| 73 | # see "Sign in to use your $X store credit" via a hint surfaced when | |
| 74 | # the email field is filled on the checkout page, not here). | |
| 75 | my $buyer_auth = MODS::RePricer::BuyerAuth->new; | |
| 76 | my $bc = MODS::RePricer::BuyerCredits->new; | |
| 77 | my $credit_cents = 0; | |
| 78 | my $credit_apply = 0; # how much of the current cart will be covered | |
| 79 | my $buyer_signed = 0; | |
| 80 | { | |
| 81 | my $b = $buyer_auth->verify($q, $db, $dbh, $DB); | |
| 82 | if ($b && $b->{id} && $b->{storefront_id} && $b->{storefront_id} eq $sid) { | |
| 83 | $buyer_signed = 1; | |
| 84 | $credit_cents = $bc->balance_cents($db, $dbh, $DB, $sid, | |
| 85 | { buyer_account_id => $b->{id} }); | |
| 86 | if ($credit_cents > 0 && $total > 0) { | |
| 87 | $credit_apply = $credit_cents > $total ? $total : $credit_cents; | |
| 88 | } | |
| 89 | } | |
| 90 | } | |
| 91 | my $total_after_credit = $total - $credit_apply; | |
| 92 | $total_after_credit = 0 if $total_after_credit < 0; | |
| 93 | ||
| 94 | $db->db_disconnect($dbh); | |
| 95 | ||
| 96 | my $applied_code = $applied ? ($applied->{code} || '') : ''; | |
| 97 | my $applied_label = $applied ? $promo->marketing_label($applied) : ''; | |
| 98 | ||
| 99 | # Flash from /cart_promo.cgi redirects (?promo_ok=... / ?promo_err=...). | |
| 100 | my $promo_flash_msg = ''; | |
| 101 | my $promo_flash_kind = ''; | |
| 102 | if (defined $form->{promo_ok}) { | |
| 103 | $promo_flash_kind = 'ok'; | |
| 104 | $promo_flash_msg = $form->{promo_ok}; | |
| 105 | } elsif (defined $form->{promo_err}) { | |
| 106 | $promo_flash_kind = 'err'; | |
| 107 | $promo_flash_msg = $form->{promo_err}; | |
| 108 | } | |
| 109 | $promo_flash_msg =~ s/[^\x20-\x7E]//g; | |
| 110 | $promo_flash_msg = substr($promo_flash_msg, 0, 200); | |
| 111 | ||
| 112 | my @items; | |
| 113 | my $needs_shipping = 0; | |
| 114 | foreach my $r (@rows) { | |
| 115 | my $hero = $r->{hero_image_url} || '/assets/no-cover.svg'; | |
| 116 | my $price = '$' . sprintf('%.2f', ($r->{unit_price_cents} || 0) / 100); | |
| 117 | my $line = '$' . sprintf('%.2f', (($r->{unit_price_cents} || 0) * ($r->{quantity} || 1)) / 100); | |
| 118 | # Variant tag -- a tiny "3D PRINT \xb7 Red PLA" pill under the | |
| 119 | # title so the buyer sees which kind/color/material they added. | |
| 120 | my $kind = $r->{purchase_kind} || 'digital'; | |
| 121 | my $variant_pill = ''; | |
| 122 | if ($kind eq 'physical') { | |
| 123 | $needs_shipping = 1; | |
| 124 | my @bits = ('3D Print'); | |
| 125 | push @bits, _h($r->{selected_color}) if $r->{selected_color}; | |
| 126 | push @bits, _h($r->{selected_material}) if $r->{selected_material}; | |
| 127 | $variant_pill = join(' · ', @bits); | |
| 128 | } elsif ($kind eq 'digital') { | |
| 129 | $variant_pill = 'Digital download'; | |
| 130 | } | |
| 131 | if ($r->{is_bundle}) { | |
| 132 | my $n = $r->{bundle_item_count} || 0; | |
| 133 | $variant_pill = ($kind eq 'physical' ? 'Bundle · 3D Print' : 'Bundle · Digital') | |
| 134 | . " · $n item(s)"; | |
| 135 | } | |
| 136 | push @items, { | |
| 137 | cart_item_id => $r->{cart_item_id}, | |
| 138 | model_id => $r->{model_id} || 0, | |
| 139 | bundle_id => $r->{bundle_id} || 0, | |
| 140 | is_bundle => $r->{is_bundle} ? 1 : 0, | |
| 141 | title => _h($r->{title}), | |
| 142 | hero => $hero, | |
| 143 | hero_pos => $r->{hero_image_pos} || '50% 50%', | |
| 144 | price => $price, | |
| 145 | line_total => $line, | |
| 146 | quantity => $r->{quantity} || 1, | |
| 147 | variant_pill => $variant_pill, | |
| 148 | is_physical => ($kind eq 'physical') ? 1 : 0, | |
| 149 | }; | |
| 150 | } | |
| 151 | ||
| 152 | # Storefront header chrome (brand + search + sign in + cart) -- same | |
| 153 | # header every store.cgi layout renders. Pulled in via the shared | |
| 154 | # helper so cart, checkout, my_orders etc. all show identical chrome. | |
| 155 | require MODS::RePricer::StoreChrome; | |
| 156 | my $chrome_html = MODS::RePricer::StoreChrome->header_html({ | |
| 157 | store_id => $store->{id}, | |
| 158 | store_name => $store->{name} || $store->{subdomain}, | |
| 159 | is_buyer_in => $buyer_signed, | |
| 160 | cart_count => scalar(@items), | |
| 161 | db => $db, | |
| 162 | dbh => $dbh, | |
| 163 | DB => $DB, | |
| 164 | }); | |
| 165 | ||
| 166 | my $tvars = { | |
| 167 | theme_css_vars => $css_vars, | |
| 168 | store_id => $store->{id}, | |
| 169 | store_name => $store->{name} || $store->{subdomain}, | |
| 170 | store_chrome_html => $chrome_html, | |
| 171 | items => \@items, | |
| 172 | has_items => scalar(@items) ? 1 : 0, | |
| 173 | item_count => scalar(@items), | |
| 174 | subtotal_display => '$' . sprintf('%.2f', $subtotal / 100), | |
| 175 | discount_display => '$' . sprintf('%.2f', $discount / 100), | |
| 176 | total_display => '$' . sprintf('%.2f', $total_after_credit / 100), | |
| 177 | total_before_credit_display => '$' . sprintf('%.2f', $total / 100), | |
| 178 | has_discount => $discount > 0 ? 1 : 0, | |
| 179 | has_applied_promo => $applied ? 1 : 0, | |
| 180 | applied_code => $applied_code, | |
| 181 | applied_label => $applied_label, | |
| 182 | credit_balance_display => '$' . sprintf('%.2f', $credit_cents / 100), | |
| 183 | credit_applied_display => '$' . sprintf('%.2f', $credit_apply / 100), | |
| 184 | has_credit => $credit_apply > 0 ? 1 : 0, | |
| 185 | has_credit_balance => $credit_cents > 0 ? 1 : 0, | |
| 186 | buyer_signed_in => $buyer_signed, | |
| 187 | promo_flash_msg => $promo_flash_msg, | |
| 188 | promo_flash_kind => $promo_flash_kind, | |
| 189 | has_promo_flash => length $promo_flash_msg ? 1 : 0, | |
| 190 | needs_shipping => $needs_shipping, | |
| 191 | back_href => "/store.cgi?id=$sid", | |
| 192 | }; | |
| 193 | ||
| 194 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n"; | |
| 195 | print "Set-Cookie: $cookie_line\n" if $cookie_line; | |
| 196 | print "\n"; | |
| 197 | ||
| 198 | print join('', $tfile->template('repricer_cart.html', $tvars, undef)); | |
| 199 | ||
| 200 | sub _h { | |
| 201 | my ($s) = @_; | |
| 202 | $s = '' unless defined $s; | |
| 203 | $s =~ s/&/&/g; $s =~ s/</</g; $s =~ s/>/>/g; | |
| 204 | $s =~ s/"/"/g; $s =~ s/'/'/g; | |
| 205 | return $s; | |
| 206 | } |