user()->role === 'admin', 403); $pendingOrders = []; $etsyNotice = null; try { $pendingOrders = $this->pendingOrders($etsy); if ($pendingOrders === [] && (! $etsy->configured() || ! EtsyConnection::query()->exists())) { $etsyNotice = 'Connect your Etsy shop to see pending orders.'; } } catch (Throwable $exception) { report($exception); $etsyNotice = 'Etsy pending orders are temporarily unavailable.'; } $account = []; $balance = []; $orders = []; $profileNotice = null; $integrationError = null; $historyAvailable = false; try { $parcel2Go->verifyCredentials(); try { $orders = $parcel2Go->fetch('me/orders/detail'); $historyAvailable = true; } catch (Throwable $exception) { $profileNotice = $parcel2Go->profileConnected() ? 'Parcel2Go account access needs attention. Reconnect the account in Settings.' : ($exception instanceof RequestException && in_array($exception->response->status(), [401, 403], true) ? 'Parcel2Go denied shipment history for the saved Client Credentials client. Check that its Client ID and secret match the client used by your previous dashboard.' : 'Parcel2Go shipment history is temporarily unavailable.'); } try { $account = $parcel2Go->fetch('me/detail'); } catch (Throwable $exception) { report($exception); } try { $balance = $parcel2Go->fetch('me/prepay/balance'); } catch (Throwable $exception) { report($exception); } } catch (Throwable $exception) { report($exception); $integrationError = 'Parcel2Go is unavailable. Check API credentials and connectivity.'; } return view('shipping', [ 'account' => $account, 'balance' => $balance, 'orders' => $orders, 'historyAvailable' => $historyAvailable, 'profileNotice' => $profileNotice, 'integrationError' => $integrationError, 'pendingOrders' => $pendingOrders, 'etsyNotice' => $etsyNotice, 'collectionPostcode' => $settings->for('parcel2go')['collection_postcode'] ?? '', 'selectedReceipt' => collect($pendingOrders)->first(fn (array $receipt): bool => (string) ($receipt['receipt_id'] ?? '') === (string) $request->session()->get('shipping_receipt_id')), 'quotes' => $request->session()->get('shipping_quotes', []), 'collectionDates' => $request->session()->get('shipping_collection_dates', []), 'selectedService' => $request->session()->get('shipping_selected_service'), 'bookingResult' => $request->session()->get('shipping_booking_result'), 'bookingPending' => $request->session()->get('shipping_booking_pending', false), ]); } public function quote(Request $request, Parcel2Go $parcel2Go, Etsy $etsy, IntegrationSettings $settings): RedirectResponse { abort_unless($request->user()->role === 'admin', 403); if ($request->session()->get('shipping_booking_pending')) { return redirect()->route('shipping')->withErrors(['collection' => 'A booking outcome is uncertain. Check your Parcel2Go account before trying again.']); } $details = $request->validate([ 'receipt_id' => ['required', 'integer', 'min:1'], 'weight' => ['required', 'numeric', 'gt:0', 'lte:30'], 'length' => ['required', 'numeric', 'gt:0', 'lte:200'], 'width' => ['required', 'numeric', 'gt:0', 'lte:200'], 'height' => ['required', 'numeric', 'gt:0', 'lte:200'], ]); try { $receipt = collect($this->pendingOrders($etsy))->first(fn (array $order): bool => (string) ($order['receipt_id'] ?? '') === (string) $details['receipt_id']); if (! $receipt) { return redirect()->route('shipping')->withErrors(['quote' => 'This Etsy order is no longer pending. Refresh the order list.']); } $collectionPostcode = $settings->for('parcel2go')['collection_postcode'] ?? null; if (blank($collectionPostcode) || blank($receipt['zip'] ?? null) || ($receipt['country_iso'] ?? 'GB') !== 'GB' || ($receipt['grandtotal']['currency_code'] ?? 'GBP') !== 'GBP') { return redirect()->route('shipping')->withErrors(['quote' => 'A UK collection postcode and a UK Etsy delivery address priced in GBP are required.']); } $total = $receipt['grandtotal'] ?? []; $value = (float) ($total['amount'] ?? 0) / max(1, (int) ($total['divisor'] ?? 1)); $details = array_merge($details, [ 'collection_postcode' => $collectionPostcode, 'delivery_postcode' => $receipt['zip'], 'value' => min(10000, max(0, $value)), ]); $quotes = $parcel2Go->quote($details)['Quotes'] ?? []; $request->session()->put('shipping_quote', $details); $request->session()->put('shipping_receipt_id', $receipt['receipt_id']); $request->session()->put('shipping_quotes', $quotes); $request->session()->forget(['shipping_collection_dates', 'shipping_selected_service', 'shipping_sender_location']); return redirect()->route('shipping')->with('status', count($quotes).' services available.'); } catch (Throwable $exception) { report($exception); return redirect()->route('shipping')->withErrors(['quote' => 'Could not quote this Etsy order. Check its delivery address and parcel details.']); } } public function collectionDates(Request $request, Parcel2Go $parcel2Go): RedirectResponse { abort_unless($request->user()->role === 'admin', 403); $data = $request->validate([ 'service' => ['required', 'string', 'max:200'], 'sender_street' => ['required', 'string', 'max:200'], 'sender_town' => ['required', 'string', 'max:100'], ]); $details = $request->session()->get('shipping_quote'); $quote = collect($request->session()->get('shipping_quotes', []))->first(fn ($quote) => ($quote['Service']['Slug'] ?? null) === $data['service'] && ($quote['Service']['CollectionType'] ?? null) === 'Collection'); if (! $details || ! $quote) { return redirect()->route('shipping')->withErrors(['service' => 'Select a collection service from a current quote.']); } try { $dates = $parcel2Go->collectionDates($data['service'], $data['sender_street'], $data['sender_town'], $details['collection_postcode'])['CollectionDates'] ?? []; $request->session()->put('shipping_collection_dates', $dates); $request->session()->put('shipping_selected_service', $data['service']); $request->session()->put('shipping_sender_location', $data); return redirect()->route('shipping')->with('status', 'Choose a collection date and enter the addresses.'); } catch (Throwable $exception) { report($exception); return redirect()->route('shipping')->withErrors(['collection' => 'Parcel2Go could not return collection dates for this service.']); } } public function bookCollection(Request $request, Parcel2Go $parcel2Go, Etsy $etsy): RedirectResponse { abort_unless($request->user()->role === 'admin', 403); if ($request->session()->get('shipping_booking_pending')) { return redirect()->route('shipping')->withErrors(['collection' => 'A booking outcome is uncertain. Check your Parcel2Go account before trying again.']); } $booking = $request->validate([ 'collection_date' => ['required', 'date', 'after:now'], 'sender_name' => ['required', 'string', 'max:100'], 'sender_email' => ['required', 'email', 'max:150'], 'sender_phone' => ['required', 'string', 'max:30'], 'sender_property' => ['required', 'string', 'max:100'], 'sender_street' => ['required', 'string', 'max:200'], 'sender_town' => ['required', 'string', 'max:100'], 'recipient_name' => ['required', 'string', 'max:100'], 'recipient_email' => ['required', 'email', 'max:150'], 'recipient_phone' => ['required', 'string', 'max:30'], 'recipient_property' => ['required', 'string', 'max:100'], 'recipient_street' => ['required', 'string', 'max:200'], 'recipient_town' => ['required', 'string', 'max:100'], 'contents' => ['required', 'string', 'max:200'], ]); $details = $request->session()->get('shipping_quote'); $service = $request->session()->get('shipping_selected_service'); $dates = $request->session()->get('shipping_collection_dates', []); $senderLocation = $request->session()->get('shipping_sender_location', []); $selectedQuote = collect($request->session()->get('shipping_quotes', [])) ->first(fn ($quote) => ($quote['Service']['Slug'] ?? null) === $service && ($quote['Service']['CollectionType'] ?? null) === 'Collection'); if (! $details || ! $service || ! collect($dates)->contains(fn ($date) => ($date['CollectionDate'] ?? null) === $booking['collection_date']) || ! $selectedQuote || ! empty($selectedQuote['RequiresCustoms']) || ! empty($selectedQuote['RequiresCommercialInvoice']) || ($senderLocation['sender_street'] ?? null) !== $booking['sender_street'] || ($senderLocation['sender_town'] ?? null) !== $booking['sender_town']) { return redirect()->route('shipping')->withErrors(['collection' => 'Choose an available collection date for the quoted address.']); } try { $receipt = collect($this->pendingOrders($etsy))->first(fn (array $order): bool => (string) ($order['receipt_id'] ?? '') === (string) $request->session()->get('shipping_receipt_id')); if (! $receipt || ($receipt['zip'] ?? null) !== $details['delivery_postcode']) { return redirect()->route('shipping')->withErrors(['collection' => 'The Etsy order or delivery postcode changed. Request a fresh quote.']); } } catch (Throwable $exception) { report($exception); return redirect()->route('shipping')->withErrors(['collection' => 'Could not verify the Etsy order before booking. Try again.']); } try { $request->session()->put('shipping_booking_pending', true); $result = $parcel2Go->bookCollection($details, $service, $booking); if (empty($result['OrderId'])) { throw new \RuntimeException('Parcel2Go did not return an order reference.'); } $request->session()->forget(['shipping_quote', 'shipping_receipt_id', 'shipping_quotes', 'shipping_collection_dates', 'shipping_selected_service', 'shipping_sender_location', 'shipping_booking_pending']); return redirect()->route('shipping')->with('shipping_booking_result', $result)->with('status', 'Collection order created. Complete payment with Parcel2Go.'); } catch (Throwable $exception) { report($exception); return redirect()->route('shipping')->withErrors(['collection' => 'Booking outcome is uncertain. Check your Parcel2Go account before trying again.']); } } public function resetBooking(Request $request): RedirectResponse { abort_unless($request->user()->role === 'admin', 403); $request->validate(['checked_account' => ['accepted']]); $request->session()->forget(['shipping_quote', 'shipping_receipt_id', 'shipping_quotes', 'shipping_collection_dates', 'shipping_selected_service', 'shipping_sender_location', 'shipping_booking_pending']); return redirect()->route('shipping')->with('status', 'Booking workflow reset. Start with a fresh quote.'); } public function label(Request $request, Parcel2Go $parcel2Go, string $orderLineId): Response { abort_unless($request->user()->role === 'admin', 403); abort_unless(ctype_digit($orderLineId), 404); $orders = $parcel2Go->fetch('me/orders/detail')['Orders'] ?? []; $order = collect($orders)->first(fn ($order) => (string) ($order['OrderLineId'] ?? '') === $orderLineId); abort_unless($order && ! empty($order['Hash']), 404); $pdf = $parcel2Go->labelPdf($orderLineId, $order['Hash']); return response($pdf, 200, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'attachment; filename="parcel2go-'.$orderLineId.'.pdf"', 'Cache-Control' => 'private, no-store', ]); } /** @return array> */ private function pendingOrders(Etsy $etsy): array { $connection = EtsyConnection::first(); return $connection && $etsy->configured() ? ($etsy->receipts($connection)['results'] ?? []) : []; } }