Diff -- /var/www/vhosts/3dshawn.com/shop.3dshawn.com/fulfill_order.cgi
Diff
/var/www/vhosts/3dshawn.com/shop.3dshawn.com/fulfill_order.cgi
added on local at 2026-07-11 18:36:32
Added
+342
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 312e1daa9816
to 312e1daa9816
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 -- Fulfillment detail (one order). | |
| 4 | # | |
| 5 | # GET /fulfill_order.cgi?order_id=N -- detail view | |
| 6 | # POST /fulfill_order.cgi -- multi-action handler: | |
| 7 | # _act=set_status order_id, new_status | |
| 8 | # _act=create_label order_id, carrier, service, tracking, ... | |
| 9 | # _act=mark_shipped label_id | |
| 10 | # _act=void_label label_id | |
| 11 | # _act=add_note order_id, note | |
| 12 | # _act=update_item item_id, new_status | |
| 13 | # _act=estimate order_id (returns inline) | |
| 14 | #====================================================================== | |
| 15 | use strict; | |
| 16 | use warnings; | |
| 17 | ||
| 18 | use lib '/var/www/vhosts/3dshawn.com/shop.3dshawn.com'; | |
| 19 | use CGI; | |
| 20 | use MODS::Template; | |
| 21 | use MODS::DBConnect; | |
| 22 | use MODS::Login; | |
| 23 | use MODS::ShopCart::Config; | |
| 24 | use MODS::ShopCart::Wrapper; | |
| 25 | use MODS::ShopCart::Permissions; | |
| 26 | use MODS::ShopCart::Shipping; | |
| 27 | ||
| 28 | $| = 1; | |
| 29 | ||
| 30 | my $q = CGI->new; | |
| 31 | my $form = $q->Vars; | |
| 32 | my $auth = MODS::Login->new; | |
| 33 | my $tfile = MODS::Template->new; | |
| 34 | my $db = MODS::DBConnect->new; | |
| 35 | my $cfg = MODS::ShopCart::Config->new; | |
| 36 | my $wrap = MODS::ShopCart::Wrapper->new; | |
| 37 | my $perm = MODS::ShopCart::Permissions->new; | |
| 38 | my $ship = MODS::ShopCart::Shipping->new; | |
| 39 | my $DB = $cfg->settings('database_name'); | |
| 40 | ||
| 41 | my $userinfo = $auth->login_verify(); | |
| 42 | unless ($userinfo) { | |
| 43 | print "Status: 302 Found\nLocation: /login.cgi?return=/fulfill.cgi\n\n"; exit; | |
| 44 | } | |
| 45 | $perm->require_feature($userinfo, 'view_orders'); | |
| 46 | ||
| 47 | my $uid = $userinfo->{user_id}; $uid =~ s/[^0-9]//g; | |
| 48 | my $owner_uid = $userinfo->{owner_user_id} || $uid; | |
| 49 | $owner_uid =~ s/[^0-9]//g; | |
| 50 | ||
| 51 | my $dbh = $db->db_connect(); | |
| 52 | ||
| 53 | # POST handler | |
| 54 | if (($ENV{REQUEST_METHOD} || '') eq 'POST') { | |
| 55 | my $act = lc($form->{_act} || ''); $act =~ s/[^a-z_]//g; | |
| 56 | my $oid = $form->{order_id} || 0; $oid =~ s/[^0-9]//g; | |
| 57 | ||
| 58 | if ($act eq 'set_status' && $oid) { | |
| 59 | my $ns = $form->{new_status} || ''; | |
| 60 | my $cascade = $form->{cascade} ? 1 : 0; | |
| 61 | $ship->update_order_status($db, $dbh, $DB, $oid, $owner_uid, $ns, | |
| 62 | cascade_items => $cascade, by => $userinfo->{display_name}); | |
| 63 | } | |
| 64 | elsif ($act eq 'create_label' && $oid) { | |
| 65 | my $lid = $ship->create_label($db, $dbh, $DB, $owner_uid, $oid, | |
| 66 | carrier => $form->{carrier}, | |
| 67 | service => $form->{service}, | |
| 68 | tracking_number => $form->{tracking_number}, | |
| 69 | tracking_url => $form->{tracking_url}, | |
| 70 | cost_cents => $form->{cost_cents}, | |
| 71 | weight_grams => $form->{weight_grams}, | |
| 72 | notes => $form->{notes}, | |
| 73 | apply_to_items => 1, | |
| 74 | ); | |
| 75 | # Auto-advance order status to shipped if we now have a tracking number. | |
| 76 | if ($lid && length($form->{tracking_number} || '')) { | |
| 77 | $ship->update_order_status($db, $dbh, $DB, $oid, $owner_uid, 'shipped', | |
| 78 | cascade_items => 1, by => $userinfo->{display_name}); | |
| 79 | $ship->mark_label_shipped($db, $dbh, $DB, $lid, $owner_uid); | |
| 80 | } | |
| 81 | } | |
| 82 | elsif ($act eq 'mark_shipped') { | |
| 83 | my $lid = $form->{label_id} || 0; $lid =~ s/[^0-9]//g; | |
| 84 | $ship->mark_label_shipped($db, $dbh, $DB, $lid, $owner_uid); | |
| 85 | } | |
| 86 | elsif ($act eq 'void_label') { | |
| 87 | my $lid = $form->{label_id} || 0; $lid =~ s/[^0-9]//g; | |
| 88 | $ship->void_label($db, $dbh, $DB, $lid, $owner_uid); | |
| 89 | } | |
| 90 | elsif ($act eq 'add_note' && $oid) { | |
| 91 | my $note = $form->{note} || ''; | |
| 92 | if (length $note) { | |
| 93 | $ship->add_note($db, $dbh, $DB, $oid, $owner_uid, $note, is_internal => 1); | |
| 94 | } | |
| 95 | } | |
| 96 | elsif ($act eq 'update_item') { | |
| 97 | my $iid = $form->{item_id} || 0; $iid =~ s/[^0-9]//g; | |
| 98 | my $ns = $form->{new_status} || ''; | |
| 99 | $ship->update_item_status($db, $dbh, $DB, $oid, $iid, $owner_uid, $ns); | |
| 100 | } | |
| 101 | ||
| 102 | $db->db_disconnect($dbh); | |
| 103 | my $tail = $oid ? "?order_id=$oid&ok=1" : '?ok=1'; | |
| 104 | print "Status: 302 Found\nLocation: /fulfill_order.cgi$tail\n\n"; exit; | |
| 105 | } | |
| 106 | ||
| 107 | my $oid = $form->{order_id} || 0; $oid =~ s/[^0-9]//g; | |
| 108 | unless ($oid) { | |
| 109 | $db->db_disconnect($dbh); | |
| 110 | print "Status: 302 Found\nLocation: /fulfill.cgi\n\n"; exit; | |
| 111 | } | |
| 112 | ||
| 113 | my $order = $ship->get_order($db, $dbh, $DB, $oid, $owner_uid); | |
| 114 | unless ($order) { | |
| 115 | $db->db_disconnect($dbh); | |
| 116 | print "Status: 302 Found\nLocation: /fulfill.cgi\n\n"; exit; | |
| 117 | } | |
| 118 | ||
| 119 | my $items = $ship->get_order_items($db, $dbh, $DB, $oid, $owner_uid); | |
| 120 | my $labels = $ship->list_labels_for_order($db, $dbh, $DB, $oid, $owner_uid); | |
| 121 | my $notes = $ship->list_notes($db, $dbh, $DB, $oid); | |
| 122 | my $settings = $ship->get_settings($db, $dbh, $DB, $owner_uid); | |
| 123 | my $carriers_cfg = $ship->list_carriers($db, $dbh, $DB, $owner_uid); | |
| 124 | my $zones = $ship->list_zones($db, $dbh, $DB, $owner_uid); | |
| 125 | ||
| 126 | # Total weight for the rate estimate | |
| 127 | my $total_weight = 0; | |
| 128 | foreach my $it (@$items) { | |
| 129 | my $w = ($it->{weight_grams} || 0) * ($it->{quantity} || 1); | |
| 130 | $total_weight += $w; | |
| 131 | } | |
| 132 | ||
| 133 | # Estimate live (without saving) | |
| 134 | my $estimate = $ship->estimate_rate($db, $dbh, $DB, $owner_uid, | |
| 135 | country => $order->{ship_country}, | |
| 136 | weight_grams => $total_weight, | |
| 137 | order_subtotal_cents => $order->{subtotal_cents} || $order->{total_amount_cents}); | |
| 138 | ||
| 139 | $db->db_disconnect($dbh); | |
| 140 | ||
| 141 | my @item_rows; | |
| 142 | foreach my $it (@$items) { | |
| 143 | my $tracking = $it->{tracking_number} || ''; | |
| 144 | my $tcarrier = $it->{tracking_carrier} || ''; | |
| 145 | my $turl = length $tracking ? $ship->tracking_url($tcarrier, $tracking) : ''; | |
| 146 | push @item_rows, { | |
| 147 | id => $it->{id}, | |
| 148 | model_title => _h($it->{model_title}), | |
| 149 | purchase_kind => $it->{purchase_kind} || '', | |
| 150 | quantity => $it->{quantity} || 1, | |
| 151 | color => _h($it->{selected_color} || ''), | |
| 152 | material => _h($it->{selected_material} || ''), | |
| 153 | price => sprintf('%.2f', ($it->{price_paid_cents} || 0) / 100), | |
| 154 | weight => $it->{weight_grams} || 0, | |
| 155 | is_physical => $it->{purchase_kind} eq 'physical' ? 1 : 0, | |
| 156 | status => $it->{fulfillment_status} || 'not_required', | |
| 157 | status_label => _status_label($it->{fulfillment_status}), | |
| 158 | status_class => 'fs-' . ($it->{fulfillment_status} || 'new'), | |
| 159 | tracking => _h($tracking), | |
| 160 | tracking_carrier=> _h($tcarrier), | |
| 161 | tracking_url => _h($turl), | |
| 162 | has_tracking => length $tracking ? 1 : 0, | |
| 163 | }; | |
| 164 | } | |
| 165 | ||
| 166 | my @label_rows; | |
| 167 | foreach my $l (@$labels) { | |
| 168 | my $turl = $l->{tracking_url} || ($l->{tracking_number} ? $ship->tracking_url($l->{carrier}, $l->{tracking_number}) : ''); | |
| 169 | push @label_rows, { | |
| 170 | id => $l->{id}, | |
| 171 | carrier => _h($l->{carrier}), | |
| 172 | carrier_label => _h($ship->carrier_label($l->{carrier})), | |
| 173 | service => _h($l->{service} || ''), | |
| 174 | tracking => _h($l->{tracking_number} || ''), | |
| 175 | tracking_url => _h($turl), | |
| 176 | has_tracking => length($l->{tracking_number} || '') ? 1 : 0, | |
| 177 | cost => defined $l->{cost_cents} ? sprintf('%.2f', $l->{cost_cents} / 100) : '', | |
| 178 | has_cost => defined $l->{cost_cents} ? 1 : 0, | |
| 179 | weight => $l->{weight_grams} || 0, | |
| 180 | status => _h($l->{status}), | |
| 181 | status_label => ucfirst($l->{status} || ''), | |
| 182 | purchased_at => _fo_ts_span($l->{purchased_at}, ''), | |
| 183 | shipped_at => _fo_ts_span($l->{shipped_at}, ''), | |
| 184 | delivered_at => _fo_ts_span($l->{delivered_at}, ''), | |
| 185 | is_active => ($l->{status} && $l->{status} ne 'voided') ? 1 : 0, | |
| 186 | is_voided => ($l->{status} && $l->{status} eq 'voided') ? 1 : 0, | |
| 187 | print_href => '/print_label.cgi?label_id=' . $l->{id}, | |
| 188 | }; | |
| 189 | } | |
| 190 | ||
| 191 | my @note_rows; | |
| 192 | foreach my $n (@$notes) { | |
| 193 | push @note_rows, { | |
| 194 | note => _h_nl($n->{note}), | |
| 195 | created_at => _fo_ts_span($n->{created_at}, ''), | |
| 196 | is_internal => $n->{is_internal} ? 1 : 0, | |
| 197 | }; | |
| 198 | } | |
| 199 | ||
| 200 | # Build carrier <select> | |
| 201 | my @carrier_opts = ( | |
| 202 | [manual => 'Manual / paper label'], | |
| 203 | [usps => 'USPS'], | |
| 204 | [ups => 'UPS'], | |
| 205 | [fedex => 'FedEx'], | |
| 206 | [dhl => 'DHL'], | |
| 207 | [other => 'Other'], | |
| 208 | ); | |
| 209 | my $carrier_opts_html = ''; | |
| 210 | foreach my $r (@carrier_opts) { | |
| 211 | $carrier_opts_html .= qq~<option value="$r->[0]">$r->[1]</option>~; | |
| 212 | } | |
| 213 | ||
| 214 | my $ship_to_html = _build_addr_block( | |
| 215 | $order->{ship_name}, undef, $order->{ship_addr1}, $order->{ship_addr2}, | |
| 216 | $order->{ship_city}, $order->{ship_region}, $order->{ship_postal}, | |
| 217 | $order->{ship_country}, $order->{ship_phone}, | |
| 218 | ); | |
| 219 | ||
| 220 | my $bill_to_html = _build_addr_block( | |
| 221 | $order->{bill_name}, undef, $order->{bill_addr1}, $order->{bill_addr2}, | |
| 222 | $order->{bill_city}, $order->{bill_region}, $order->{bill_postal}, | |
| 223 | undef, $order->{bill_phone}, | |
| 224 | ); | |
| 225 | ||
| 226 | my $origin_html = $settings ? _build_addr_block( | |
| 227 | $settings->{origin_name}, $settings->{origin_company}, | |
| 228 | $settings->{origin_line1}, $settings->{origin_line2}, | |
| 229 | $settings->{origin_city}, $settings->{origin_region}, $settings->{origin_postal}, | |
| 230 | $settings->{origin_country}, $settings->{origin_phone}, | |
| 231 | ) : ''; | |
| 232 | ||
| 233 | # Estimate display is rendered directly in the template via the | |
| 234 | # $est_dollars / $est_zone / $est_min / $est_max tvars below. | |
| 235 | ||
| 236 | my $tvars = { | |
| 237 | order_id => $order->{id}, | |
| 238 | order_status => $order->{fulfillment_status} || 'new', | |
| 239 | order_status_label=> _status_label($order->{fulfillment_status} || 'new'), | |
| 240 | order_status_class=> 'fs-' . ($order->{fulfillment_status} || 'new'), | |
| 241 | pay_status => $order->{status} || '', | |
| 242 | buyer_email => _h($order->{buyer_email}), | |
| 243 | total => sprintf('%.2f', ($order->{total_amount_cents} || 0) / 100), | |
| 244 | subtotal => sprintf('%.2f', ($order->{subtotal_cents} || 0) / 100), | |
| 245 | shipping_paid => sprintf('%.2f', ($order->{shipping_cents} || 0) / 100), | |
| 246 | tax_amount => sprintf('%.2f', ($order->{tax_amount_cents} || 0) / 100), | |
| 247 | currency => $order->{currency} || 'USD', | |
| 248 | created_at => _fo_ts_span($order->{created_at}, ''), | |
| 249 | paid_at => _fo_ts_span($order->{paid_at}, ''), | |
| 250 | has_paid_at => $order->{paid_at} ? 1 : 0, | |
| 251 | ship_to_html => $ship_to_html, | |
| 252 | bill_to_html => $bill_to_html, | |
| 253 | has_bill_to => length($order->{bill_addr1} || '') ? 1 : 0, | |
| 254 | origin_html => $origin_html, | |
| 255 | has_origin => $settings && $settings->{origin_line1} ? 1 : 0, | |
| 256 | items => \@item_rows, | |
| 257 | has_items => scalar(@item_rows) ? 1 : 0, | |
| 258 | labels => \@label_rows, | |
| 259 | has_labels => scalar(@label_rows) ? 1 : 0, | |
| 260 | notes => \@note_rows, | |
| 261 | has_notes => scalar(@note_rows) ? 1 : 0, | |
| 262 | carrier_opts_html => $carrier_opts_html, | |
| 263 | total_weight => $total_weight, | |
| 264 | has_weight => $total_weight > 0 ? 1 : 0, | |
| 265 | weight_kg => sprintf('%.2f', $total_weight / 1000), | |
| 266 | est_dollars => $estimate ? sprintf('%.2f', $estimate->{cents} / 100) : '', | |
| 267 | est_zone => $estimate ? _h($estimate->{zone_name} || '') : '', | |
| 268 | est_formula => $estimate ? _h($estimate->{formula} || '') : '', | |
| 269 | est_min => $estimate && defined $estimate->{delivery_days_min} ? $estimate->{delivery_days_min} : '', | |
| 270 | est_max => $estimate && defined $estimate->{delivery_days_max} ? $estimate->{delivery_days_max} : '', | |
| 271 | has_estimate => $estimate ? 1 : 0, | |
| 272 | has_zones => scalar(@$zones) ? 1 : 0, | |
| 273 | flash_msg => (defined $form->{ok} ? 'Saved.' : ''), | |
| 274 | flash_kind => 'ok', | |
| 275 | has_flash => (defined $form->{ok}) ? 1 : 0, | |
| 276 | settings_url => '/shipping_settings.cgi', | |
| 277 | }; | |
| 278 | ||
| 279 | print "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"; | |
| 280 | my $body = join('', $tfile->template('shopcart_fulfill_order.html', $tvars, $userinfo)); | |
| 281 | $wrap->render({ | |
| 282 | userinfo => $userinfo, | |
| 283 | page_key => 'fulfillment', | |
| 284 | title => 'Order #' . $order->{id}, | |
| 285 | body => $body, | |
| 286 | }); | |
| 287 | exit; | |
| 288 | ||
| 289 | sub _h { | |
| 290 | my $s = shift // ''; | |
| 291 | $s =~ s/&/&/g; $s =~ s/</</g; $s =~ s/>/>/g; | |
| 292 | $s =~ s/"/"/g; $s =~ s/'/'/g; | |
| 293 | return $s; | |
| 294 | } | |
| 295 | sub _h_nl { | |
| 296 | my $s = _h($_[0]); | |
| 297 | $s =~ s/\n/<br>/g; | |
| 298 | return $s; | |
| 299 | } | |
| 300 | sub _fo_ts_span { | |
| 301 | my ($s, $fallback) = @_; | |
| 302 | $fallback = '' unless defined $fallback; | |
| 303 | return $fallback unless $s && length $s; | |
| 304 | return _h($s) unless $s =~ /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/; | |
| 305 | require Time::Local; | |
| 306 | my $ep = eval { Time::Local::timelocal($6,$5,$4,$3,$2-1,$1-1900) } || 0; | |
| 307 | return _h($s) unless $ep; | |
| 308 | return qq~<span class="ts" data-ts="$ep" data-fmt="datetime">${\ _h($s) }</span>~; | |
| 309 | } | |
| 310 | sub _build_addr_block { | |
| 311 | my ($name, $company, $l1, $l2, $city, $region, $postal, $country, $phone) = @_; | |
| 312 | my @lines; | |
| 313 | push @lines, _h($name) if length($name || ''); | |
| 314 | push @lines, _h($company) if length($company || ''); | |
| 315 | push @lines, _h($l1) if length($l1 || ''); | |
| 316 | push @lines, _h($l2) if length($l2 || ''); | |
| 317 | my $csz = join(', ', grep { length } (_h($city || ''), join(' ', grep { length } (_h($region || ''), _h($postal || ''))))); | |
| 318 | push @lines, $csz if length $csz; | |
| 319 | push @lines, _h($country) if length($country || ''); | |
| 320 | push @lines, '<span style="color:var(--col-text-3)">' . _h($phone) . '</span>' if length($phone || ''); | |
| 321 | return @lines ? join('<br>', @lines) : '<em style="color:var(--col-text-3)">(no address)</em>'; | |
| 322 | } | |
| 323 | sub _status_label { | |
| 324 | my $s = shift; | |
| 325 | return { | |
| 326 | new => 'New', | |
| 327 | not_required => 'Digital only', | |
| 328 | pending => 'Pending', | |
| 329 | processing => 'Processing', | |
| 330 | printing => 'Printing', | |
| 331 | printed => 'Printed', | |
| 332 | packed => 'Packed', | |
| 333 | shipped => 'Shipped', | |
| 334 | partially_shipped => 'Partially shipped', | |
| 335 | delivered => 'Delivered', | |
| 336 | completed => 'Completed', | |
| 337 | on_hold => 'On hold', | |
| 338 | cancelled => 'Cancelled', | |
| 339 | canceled => 'Cancelled', | |
| 340 | returned => 'Returned', | |
| 341 | }->{$s || ''} || ucfirst($s || 'New'); | |
| 342 | } |