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

O Operator
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
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 -- 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#======================================================================
15use strict;
16use warnings;
17
18use lib '/var/www/vhosts/3dshawn.com/shop.3dshawn.com';
19use CGI;
20use MODS::Template;
21use MODS::DBConnect;
22use MODS::Login;
23use MODS::ShopCart::Config;
24use MODS::ShopCart::Wrapper;
25use MODS::ShopCart::Permissions;
26use MODS::ShopCart::Shipping;
27
28$| = 1;
29
30my $q = CGI->new;
31my $form = $q->Vars;
32my $auth = MODS::Login->new;
33my $tfile = MODS::Template->new;
34my $db = MODS::DBConnect->new;
35my $cfg = MODS::ShopCart::Config->new;
36my $wrap = MODS::ShopCart::Wrapper->new;
37my $perm = MODS::ShopCart::Permissions->new;
38my $ship = MODS::ShopCart::Shipping->new;
39my $DB = $cfg->settings('database_name');
40
41my $userinfo = $auth->login_verify();
42unless ($userinfo) {
43 print "Status: 302 Found\nLocation: /login.cgi?return=/fulfill.cgi\n\n"; exit;
44}
45$perm->require_feature($userinfo, 'view_orders');
46
47my $uid = $userinfo->{user_id}; $uid =~ s/[^0-9]//g;
48my $owner_uid = $userinfo->{owner_user_id} || $uid;
49$owner_uid =~ s/[^0-9]//g;
50
51my $dbh = $db->db_connect();
52
53# POST handler
54if (($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
107my $oid = $form->{order_id} || 0; $oid =~ s/[^0-9]//g;
108unless ($oid) {
109 $db->db_disconnect($dbh);
110 print "Status: 302 Found\nLocation: /fulfill.cgi\n\n"; exit;
111}
112
113my $order = $ship->get_order($db, $dbh, $DB, $oid, $owner_uid);
114unless ($order) {
115 $db->db_disconnect($dbh);
116 print "Status: 302 Found\nLocation: /fulfill.cgi\n\n"; exit;
117}
118
119my $items = $ship->get_order_items($db, $dbh, $DB, $oid, $owner_uid);
120my $labels = $ship->list_labels_for_order($db, $dbh, $DB, $oid, $owner_uid);
121my $notes = $ship->list_notes($db, $dbh, $DB, $oid);
122my $settings = $ship->get_settings($db, $dbh, $DB, $owner_uid);
123my $carriers_cfg = $ship->list_carriers($db, $dbh, $DB, $owner_uid);
124my $zones = $ship->list_zones($db, $dbh, $DB, $owner_uid);
125
126# Total weight for the rate estimate
127my $total_weight = 0;
128foreach my $it (@$items) {
129 my $w = ($it->{weight_grams} || 0) * ($it->{quantity} || 1);
130 $total_weight += $w;
131}
132
133# Estimate live (without saving)
134my $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
141my @item_rows;
142foreach 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
166my @label_rows;
167foreach 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
191my @note_rows;
192foreach 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>
201my @carrier_opts = (
202 [manual => 'Manual / paper label'],
203 [usps => 'USPS'],
204 [ups => 'UPS'],
205 [fedex => 'FedEx'],
206 [dhl => 'DHL'],
207 [other => 'Other'],
208);
209my $carrier_opts_html = '';
210foreach my $r (@carrier_opts) {
211 $carrier_opts_html .= qq~<option value="$r->[0]">$r->[1]</option>~;
212}
213
214my $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
220my $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
226my $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
236my $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
279print "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";
280my $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});
287exit;
288
289sub _h {
290 my $s = shift // '';
291 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
292 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
293 return $s;
294}
295sub _h_nl {
296 my $s = _h($_[0]);
297 $s =~ s/\n/<br>/g;
298 return $s;
299}
300sub _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}
310sub _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}
323sub _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}