Tie Parcel2Go shipping to pending Etsy orders
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 26s
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 26s
This commit is contained in:
@@ -47,11 +47,11 @@ class DashboardController extends Controller
|
||||
if ($isAdmin && filled($shippingConfig['client_id'] ?? null) && filled($shippingConfig['client_secret'] ?? null) && filled($shippingConfig['auth_url'] ?? null)) {
|
||||
try {
|
||||
$parcel2Go->verifyCredentials();
|
||||
if ($parcel2Go->hasScope('my-profile')) {
|
||||
try {
|
||||
$shipments = array_slice($parcel2Go->fetch('me/orders/detail')['Orders'] ?? [], 0, 4);
|
||||
$shippingStatus = 'Live account';
|
||||
} else {
|
||||
$shippingStatus = 'Quotes available';
|
||||
} catch (Throwable $exception) {
|
||||
$shippingStatus = 'History access needed';
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
@@ -36,6 +36,7 @@ class EtsySettingsController extends Controller
|
||||
'auth_url' => $parcel2go['auth_url'] ?? '',
|
||||
'base_url' => $parcel2go['base_url'] ?? '',
|
||||
'scope' => $parcel2go['scope'] ?? 'public-api',
|
||||
'collection_postcode' => $parcel2go['collection_postcode'] ?? '',
|
||||
],
|
||||
'bambuddy' => [
|
||||
'stored' => in_array('bambuddy', $storedIntegrations, true),
|
||||
|
||||
@@ -21,6 +21,7 @@ class IntegrationSettingsController extends Controller
|
||||
'auth_url' => ['nullable', 'url:https', 'max:2048'],
|
||||
'base_url' => ['nullable', 'url:https', 'max:2048'],
|
||||
'scope' => ['nullable', 'string', 'in:public-api,public-api my-profile,public-api payment,public-api payment my-profile'],
|
||||
'collection_postcode' => ['nullable', 'string', 'max:12'],
|
||||
],
|
||||
default => [
|
||||
'url' => ['nullable', 'url:https', 'max:2048'],
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\EtsyConnection;
|
||||
use App\Services\Etsy;
|
||||
use App\Services\IntegrationSettings;
|
||||
use App\Services\Parcel2Go;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
@@ -11,82 +15,105 @@ use Throwable;
|
||||
|
||||
class ShippingController extends Controller
|
||||
{
|
||||
public function index(Request $request, Parcel2Go $parcel2Go): View
|
||||
public function index(Request $request, Parcel2Go $parcel2Go, Etsy $etsy, IntegrationSettings $settings): View
|
||||
{
|
||||
abort_unless($request->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();
|
||||
$account = [];
|
||||
$balance = [];
|
||||
$orders = [];
|
||||
$profileAvailable = $parcel2Go->hasScope('my-profile');
|
||||
$profileNotice = null;
|
||||
|
||||
if ($profileAvailable) {
|
||||
try {
|
||||
$orders = $parcel2Go->fetch('me/orders/detail');
|
||||
$historyAvailable = true;
|
||||
} catch (Throwable $exception) {
|
||||
$profileNotice = $exception instanceof RequestException && in_array($exception->response->status(), [401, 403], true)
|
||||
? 'Parcel2Go denied shipment history for this client. Request my-profile access from Parcel2Go API support.'
|
||||
: 'Parcel2Go shipment history is temporarily unavailable.';
|
||||
}
|
||||
if ($parcel2Go->hasScope('my-profile')) {
|
||||
try {
|
||||
$account = $parcel2Go->fetch('me/detail');
|
||||
$balance = $parcel2Go->fetch('me/prepay/balance');
|
||||
$orders = $parcel2Go->fetch('me/orders/detail');
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$profileNotice = 'Parcel2Go connected, but account history is unavailable. Check that this client has my-profile access.';
|
||||
}
|
||||
} else {
|
||||
$profileNotice = $parcel2Go->scopeWasReduced()
|
||||
? 'Parcel2Go rejected the selected API access. Basic shipping access works, but account history and labels need my-profile permission for this client.'
|
||||
: 'Parcel2Go connected for quotes and collections. Add my-profile access in Parcel2Go to show account history and labels.';
|
||||
}
|
||||
|
||||
return view('shipping', compact('account', 'balance', 'orders', 'profileAvailable', 'profileNotice') + [
|
||||
'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),
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return view('shipping', [
|
||||
'account' => [], 'balance' => [], 'orders' => [],
|
||||
'profileAvailable' => false,
|
||||
'profileNotice' => null,
|
||||
'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),
|
||||
'integrationError' => 'Parcel2Go is unavailable. Check API credentials and connectivity.',
|
||||
]);
|
||||
$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): RedirectResponse
|
||||
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([
|
||||
'collection_postcode' => ['required', 'string', 'max:12'],
|
||||
'delivery_postcode' => ['required', 'string', 'max:12'],
|
||||
'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'],
|
||||
'value' => ['required', 'numeric', 'gte:0', 'lte:10000'],
|
||||
]);
|
||||
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']);
|
||||
$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' => 'Parcel2Go could not quote this parcel. Check the postcodes and parcel details.']);
|
||||
return redirect()->route('shipping')->withErrors(['quote' => 'Could not quote this Etsy order. Check its delivery address and parcel details.']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +145,7 @@ class ShippingController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function bookCollection(Request $request, Parcel2Go $parcel2Go): RedirectResponse
|
||||
public function bookCollection(Request $request, Parcel2Go $parcel2Go, Etsy $etsy): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
if ($request->session()->get('shipping_booking_pending')) {
|
||||
@@ -152,13 +179,24 @@ class ShippingController extends Controller
|
||||
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_quotes', 'shipping_collection_dates', 'shipping_selected_service', 'shipping_sender_location', 'shipping_booking_pending']);
|
||||
$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) {
|
||||
@@ -172,7 +210,7 @@ class ShippingController extends Controller
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
$request->validate(['checked_account' => ['accepted']]);
|
||||
$request->session()->forget(['shipping_quote', 'shipping_quotes', 'shipping_collection_dates', 'shipping_selected_service', 'shipping_sender_location', 'shipping_booking_pending']);
|
||||
$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.');
|
||||
}
|
||||
@@ -192,4 +230,12 @@ class ShippingController extends Controller
|
||||
'Cache-Control' => 'private, no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
private function pendingOrders(Etsy $etsy): array
|
||||
{
|
||||
$connection = EtsyConnection::first();
|
||||
|
||||
return $connection && $etsy->configured() ? ($etsy->receipts($connection)['results'] ?? []) : [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user