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)) {
|
if ($isAdmin && filled($shippingConfig['client_id'] ?? null) && filled($shippingConfig['client_secret'] ?? null) && filled($shippingConfig['auth_url'] ?? null)) {
|
||||||
try {
|
try {
|
||||||
$parcel2Go->verifyCredentials();
|
$parcel2Go->verifyCredentials();
|
||||||
if ($parcel2Go->hasScope('my-profile')) {
|
try {
|
||||||
$shipments = array_slice($parcel2Go->fetch('me/orders/detail')['Orders'] ?? [], 0, 4);
|
$shipments = array_slice($parcel2Go->fetch('me/orders/detail')['Orders'] ?? [], 0, 4);
|
||||||
$shippingStatus = 'Live account';
|
$shippingStatus = 'Live account';
|
||||||
} else {
|
} catch (Throwable $exception) {
|
||||||
$shippingStatus = 'Quotes available';
|
$shippingStatus = 'History access needed';
|
||||||
}
|
}
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
report($exception);
|
report($exception);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class EtsySettingsController extends Controller
|
|||||||
'auth_url' => $parcel2go['auth_url'] ?? '',
|
'auth_url' => $parcel2go['auth_url'] ?? '',
|
||||||
'base_url' => $parcel2go['base_url'] ?? '',
|
'base_url' => $parcel2go['base_url'] ?? '',
|
||||||
'scope' => $parcel2go['scope'] ?? 'public-api',
|
'scope' => $parcel2go['scope'] ?? 'public-api',
|
||||||
|
'collection_postcode' => $parcel2go['collection_postcode'] ?? '',
|
||||||
],
|
],
|
||||||
'bambuddy' => [
|
'bambuddy' => [
|
||||||
'stored' => in_array('bambuddy', $storedIntegrations, true),
|
'stored' => in_array('bambuddy', $storedIntegrations, true),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class IntegrationSettingsController extends Controller
|
|||||||
'auth_url' => ['nullable', 'url:https', 'max:2048'],
|
'auth_url' => ['nullable', 'url:https', 'max:2048'],
|
||||||
'base_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'],
|
'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 => [
|
default => [
|
||||||
'url' => ['nullable', 'url:https', 'max:2048'],
|
'url' => ['nullable', 'url:https', 'max:2048'],
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\EtsyConnection;
|
||||||
|
use App\Services\Etsy;
|
||||||
|
use App\Services\IntegrationSettings;
|
||||||
use App\Services\Parcel2Go;
|
use App\Services\Parcel2Go;
|
||||||
|
use Illuminate\Http\Client\RequestException;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
@@ -11,82 +15,105 @@ use Throwable;
|
|||||||
|
|
||||||
class ShippingController extends Controller
|
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);
|
abort_unless($request->user()->role === 'admin', 403);
|
||||||
|
$pendingOrders = [];
|
||||||
|
$etsyNotice = null;
|
||||||
try {
|
try {
|
||||||
$parcel2Go->verifyCredentials();
|
$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 = [];
|
$account = [];
|
||||||
$balance = [];
|
$balance = [];
|
||||||
$orders = [];
|
$orders = [];
|
||||||
$profileAvailable = $parcel2Go->hasScope('my-profile');
|
|
||||||
$profileNotice = null;
|
$profileNotice = null;
|
||||||
|
$integrationError = null;
|
||||||
if ($profileAvailable) {
|
$historyAvailable = false;
|
||||||
|
try {
|
||||||
|
$parcel2Go->verifyCredentials();
|
||||||
|
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 {
|
try {
|
||||||
$account = $parcel2Go->fetch('me/detail');
|
$account = $parcel2Go->fetch('me/detail');
|
||||||
$balance = $parcel2Go->fetch('me/prepay/balance');
|
$balance = $parcel2Go->fetch('me/prepay/balance');
|
||||||
$orders = $parcel2Go->fetch('me/orders/detail');
|
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
report($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) {
|
} catch (Throwable $exception) {
|
||||||
report($exception);
|
report($exception);
|
||||||
|
$integrationError = 'Parcel2Go is unavailable. Check API credentials and connectivity.';
|
||||||
|
}
|
||||||
|
|
||||||
return view('shipping', [
|
return view('shipping', [
|
||||||
'account' => [], 'balance' => [], 'orders' => [],
|
'account' => $account, 'balance' => $balance, 'orders' => $orders,
|
||||||
'profileAvailable' => false,
|
'historyAvailable' => $historyAvailable, 'profileNotice' => $profileNotice,
|
||||||
'profileNotice' => null,
|
'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', []),
|
'quotes' => $request->session()->get('shipping_quotes', []),
|
||||||
'collectionDates' => $request->session()->get('shipping_collection_dates', []),
|
'collectionDates' => $request->session()->get('shipping_collection_dates', []),
|
||||||
'selectedService' => $request->session()->get('shipping_selected_service'),
|
'selectedService' => $request->session()->get('shipping_selected_service'),
|
||||||
'bookingResult' => $request->session()->get('shipping_booking_result'),
|
'bookingResult' => $request->session()->get('shipping_booking_result'),
|
||||||
'bookingPending' => $request->session()->get('shipping_booking_pending', false),
|
'bookingPending' => $request->session()->get('shipping_booking_pending', false),
|
||||||
'integrationError' => 'Parcel2Go is unavailable. Check API credentials and connectivity.',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
abort_unless($request->user()->role === 'admin', 403);
|
||||||
if ($request->session()->get('shipping_booking_pending')) {
|
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.']);
|
return redirect()->route('shipping')->withErrors(['collection' => 'A booking outcome is uncertain. Check your Parcel2Go account before trying again.']);
|
||||||
}
|
}
|
||||||
$details = $request->validate([
|
$details = $request->validate([
|
||||||
'collection_postcode' => ['required', 'string', 'max:12'],
|
'receipt_id' => ['required', 'integer', 'min:1'],
|
||||||
'delivery_postcode' => ['required', 'string', 'max:12'],
|
|
||||||
'weight' => ['required', 'numeric', 'gt:0', 'lte:30'],
|
'weight' => ['required', 'numeric', 'gt:0', 'lte:30'],
|
||||||
'length' => ['required', 'numeric', 'gt:0', 'lte:200'],
|
'length' => ['required', 'numeric', 'gt:0', 'lte:200'],
|
||||||
'width' => ['required', 'numeric', 'gt:0', 'lte:200'],
|
'width' => ['required', 'numeric', 'gt:0', 'lte:200'],
|
||||||
'height' => ['required', 'numeric', 'gt:0', 'lte:200'],
|
'height' => ['required', 'numeric', 'gt:0', 'lte:200'],
|
||||||
'value' => ['required', 'numeric', 'gte:0', 'lte:10000'],
|
|
||||||
]);
|
]);
|
||||||
try {
|
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'] ?? [];
|
$quotes = $parcel2Go->quote($details)['Quotes'] ?? [];
|
||||||
$request->session()->put('shipping_quote', $details);
|
$request->session()->put('shipping_quote', $details);
|
||||||
|
$request->session()->put('shipping_receipt_id', $receipt['receipt_id']);
|
||||||
$request->session()->put('shipping_quotes', $quotes);
|
$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.');
|
return redirect()->route('shipping')->with('status', count($quotes).' services available.');
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
report($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);
|
abort_unless($request->user()->role === 'admin', 403);
|
||||||
if ($request->session()->get('shipping_booking_pending')) {
|
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.']);
|
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 {
|
try {
|
||||||
$request->session()->put('shipping_booking_pending', true);
|
$request->session()->put('shipping_booking_pending', true);
|
||||||
$result = $parcel2Go->bookCollection($details, $service, $booking);
|
$result = $parcel2Go->bookCollection($details, $service, $booking);
|
||||||
if (empty($result['OrderId'])) {
|
if (empty($result['OrderId'])) {
|
||||||
throw new \RuntimeException('Parcel2Go did not return an order reference.');
|
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.');
|
return redirect()->route('shipping')->with('shipping_booking_result', $result)->with('status', 'Collection order created. Complete payment with Parcel2Go.');
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
@@ -172,7 +210,7 @@ class ShippingController extends Controller
|
|||||||
{
|
{
|
||||||
abort_unless($request->user()->role === 'admin', 403);
|
abort_unless($request->user()->role === 'admin', 403);
|
||||||
$request->validate(['checked_account' => ['accepted']]);
|
$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.');
|
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',
|
'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'] ?? []) : [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<label class="grid gap-2 text-sm text-slate-300">Client secret<input name="client_secret" type="password" autocomplete="new-password" maxlength="255" placeholder="Leave blank to keep current value" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
<label class="grid gap-2 text-sm text-slate-300">Client secret<input name="client_secret" type="password" autocomplete="new-password" maxlength="255" placeholder="Leave blank to keep current value" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">OAuth token URL<input name="auth_url" type="url" maxlength="2048" value="{{ old('auth_url', $integrations['parcel2go']['auth_url']) }}" placeholder="https://…" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
<label class="grid gap-2 text-sm text-slate-300">OAuth token URL<input name="auth_url" type="url" maxlength="2048" value="{{ old('auth_url', $integrations['parcel2go']['auth_url']) }}" placeholder="https://…" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">API base URL<input name="base_url" type="url" maxlength="2048" value="{{ old('base_url', $integrations['parcel2go']['base_url']) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
<label class="grid gap-2 text-sm text-slate-300">API base URL<input name="base_url" type="url" maxlength="2048" value="{{ old('base_url', $integrations['parcel2go']['base_url']) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
||||||
|
<label class="grid gap-2 text-sm text-slate-300">Collection postcode<input name="collection_postcode" maxlength="12" value="{{ old('collection_postcode', $integrations['parcel2go']['collection_postcode']) }}" placeholder="Used for Etsy order shipping quotes" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">API access
|
<label class="grid gap-2 text-sm text-slate-300">API access
|
||||||
<select name="scope" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100">
|
<select name="scope" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100">
|
||||||
@foreach(['public-api' => 'Shipping quotes and collections', 'public-api my-profile' => 'Shipping and account history', 'public-api payment' => 'Shipping and PrePay payments', 'public-api payment my-profile' => 'Shipping, account history and PrePay'] as $scope => $label)
|
@foreach(['public-api' => 'Shipping quotes and collections', 'public-api my-profile' => 'Shipping and account history', 'public-api payment' => 'Shipping and PrePay payments', 'public-api payment my-profile' => 'Shipping, account history and PrePay'] as $scope => $label)
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<p class="text-xs text-slate-500">Choose account history only if Parcel2Go granted the <code>my-profile</code> scope to this client. An ungranted scope prevents sign in.</p>
|
<p class="text-xs text-slate-500">To request account history, sign in to Parcel2Go My Account → Settings → API and check this client's permissions. If <code>my-profile</code> is unavailable, email <a class="text-brand hover:underline" href="mailto:apihelp@parcel2go.com">apihelp@parcel2go.com</a> with your account email and Client ID. Ask them to enable <code>my-profile</code> for your live client. Never email the Client secret. After confirmation, select “Shipping and account history” here.</p>
|
||||||
<button class="w-fit rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save Parcel2Go</button>
|
<button class="w-fit rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save Parcel2Go</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,23 +1,41 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'Shipping')
|
@section('title', 'Shipping')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Dispatch</p><h2 class="mt-2 text-2xl font-bold">Parcel2Go</h2><p class="mt-1 text-slate-400">Compare services, arrange a collection and download labels.</p></div>
|
<div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Dispatch</p><h2 class="mt-2 text-2xl font-bold">Orders and shipping</h2><p class="mt-1 text-slate-400">Prepare pending Etsy orders and follow Parcel2Go shipments.</p></div>
|
||||||
@isset($integrationError)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">{{ $integrationError }}</div>@endisset
|
@isset($integrationError)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">{{ $integrationError }}</div>@endisset
|
||||||
@if($profileNotice)<div role="status" class="rounded-xl border border-slate-700 bg-slate-900 p-4 text-sm text-slate-300">{{ $profileNotice }}</div>@endif
|
@if($etsyNotice)<div role="status" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">{{ $etsyNotice }} <a href="{{ route('settings.index') }}" class="font-semibold underline">Settings</a></div>@endif
|
||||||
|
@if($profileNotice)<div role="status" class="rounded-xl border border-slate-700 bg-slate-900 p-4 text-sm text-slate-300">{{ $profileNotice }} See the access steps in <a href="{{ route('settings.index') }}" class="font-semibold text-brand underline">Settings</a>.</div>@endif
|
||||||
@if($bookingPending)<section role="alert" class="rounded-2xl border border-amber-500/40 bg-amber-500/10 p-6"><h3 class="font-semibold text-amber-200">Check Parcel2Go before retrying</h3><p class="mt-2 text-sm text-amber-100/80">The last booking request had no confirmed result. Check your account for the collection order, then reset this form.</p><form method="post" action="{{ route('shipping.reset-booking') }}" class="mt-4 flex flex-wrap items-center gap-3">@csrf<label class="text-sm"><input type="checkbox" name="checked_account" value="1" required> I checked the Parcel2Go account</label><button class="rounded-xl border border-amber-500/50 px-4 py-2 text-sm font-semibold text-amber-200">Reset booking form</button></form></section>@endif
|
@if($bookingPending)<section role="alert" class="rounded-2xl border border-amber-500/40 bg-amber-500/10 p-6"><h3 class="font-semibold text-amber-200">Check Parcel2Go before retrying</h3><p class="mt-2 text-sm text-amber-100/80">The last booking request had no confirmed result. Check your account for the collection order, then reset this form.</p><form method="post" action="{{ route('shipping.reset-booking') }}" class="mt-4 flex flex-wrap items-center gap-3">@csrf<label class="text-sm"><input type="checkbox" name="checked_account" value="1" required> I checked the Parcel2Go account</label><button class="rounded-xl border border-amber-500/50 px-4 py-2 text-sm font-semibold text-amber-200">Reset booking form</button></form></section>@endif
|
||||||
@if($bookingResult)
|
@if($bookingResult)
|
||||||
@php $paymentUrl = $bookingResult['Links']['payment'] ?? null; $paymentHost = is_string($paymentUrl) ? parse_url($paymentUrl, PHP_URL_HOST) : null; @endphp
|
@php $paymentUrl = $bookingResult['Links']['payment'] ?? null; $paymentHost = is_string($paymentUrl) ? parse_url($paymentUrl, PHP_URL_HOST) : null; @endphp
|
||||||
<section class="rounded-2xl border border-brand/40 bg-brand/10 p-6"><h3 class="text-lg font-semibold">Collection order #{{ $bookingResult['OrderId'] ?? '—' }} created</h3><p class="mt-2 text-sm text-slate-300">Total: £{{ number_format((float) ($bookingResult['TotalPrice'] ?? 0), 2) }}. Payment and collection confirmation are completed with Parcel2Go.</p>@if($paymentHost && ($paymentHost === 'parcel2go.com' || str_ends_with($paymentHost, '.parcel2go.com')))<a href="{{ $paymentUrl }}" target="_blank" rel="noopener noreferrer" class="mt-4 inline-flex rounded-xl bg-brand px-5 py-3 font-semibold text-slate-950">Complete payment ↗</a>@endif</section>
|
<section class="rounded-2xl border border-brand/40 bg-brand/10 p-6"><h3 class="text-lg font-semibold">Collection order #{{ $bookingResult['OrderId'] ?? '—' }} created</h3><p class="mt-2 text-sm text-slate-300">Total: £{{ number_format((float) ($bookingResult['TotalPrice'] ?? 0), 2) }}. Payment and collection confirmation are completed with Parcel2Go.</p>@if($paymentHost && ($paymentHost === 'parcel2go.com' || str_ends_with($paymentHost, '.parcel2go.com')))<a href="{{ $paymentUrl }}" target="_blank" rel="noopener noreferrer" class="mt-4 inline-flex rounded-xl bg-brand px-5 py-3 font-semibold text-slate-950">Complete payment ↗</a>@endif</section>
|
||||||
@endif
|
@endif
|
||||||
@if($profileAvailable)<div class="grid gap-4 md:grid-cols-2"><div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">Account</p><p class="mt-4 text-xl font-semibold">{{ trim(($account['Forename'] ?? '').' '.($account['Surname'] ?? '')) ?: 'Unavailable' }}</p><p class="mt-2 text-sm text-slate-400">{{ $account['Email'] ?? '' }}</p></div><div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">Prepay balance</p><p class="mt-4 text-3xl font-bold">{{ isset($balance['Balance']) ? '£'.number_format((float) $balance['Balance'], 2) : 'Unavailable' }}</p></div></div>@endif
|
@if($account || $balance)<div class="grid gap-4 md:grid-cols-2"><div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">Account</p><p class="mt-4 text-xl font-semibold">{{ trim(($account['Forename'] ?? '').' '.($account['Surname'] ?? '')) ?: 'Unavailable' }}</p><p class="mt-2 text-sm text-slate-400">{{ $account['Email'] ?? '' }}</p></div><div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">Prepay balance</p><p class="mt-4 text-3xl font-bold">{{ isset($balance['Balance']) ? '£'.number_format((float) $balance['Balance'], 2) : 'Unavailable' }}</p></div></div>@endif
|
||||||
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><div class="mb-5"><h3 class="text-lg font-semibold">1. Compare services</h3><p class="mt-1 text-sm text-slate-400">Enter a UK parcel. Dimensions are centimetres and weight is kilograms.</p></div><form method="post" action="{{ route('shipping.quote') }}" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">@csrf
|
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
||||||
<label class="grid gap-2 text-sm">Collection postcode<input name="collection_postcode" value="{{ old('collection_postcode') }}" required maxlength="12" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3"></label>
|
<div class="flex flex-wrap items-start justify-between gap-3"><div><h3 class="text-lg font-semibold">Pending Etsy orders</h3><p class="mt-1 text-sm text-slate-400">Quotes use the Etsy delivery postcode and order value. Enter the packed parcel size and weight.</p></div><span class="rounded-full bg-brand/10 px-3 py-1 text-sm font-semibold text-brand">{{ count($pendingOrders) }} pending</span></div>
|
||||||
<label class="grid gap-2 text-sm">Delivery postcode<input name="delivery_postcode" value="{{ old('delivery_postcode') }}" required maxlength="12" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3"></label>
|
@if(blank($collectionPostcode))<p class="mt-4 rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">Set your collection postcode once in <a class="font-semibold underline" href="{{ route('settings.index') }}">Parcel2Go settings</a> to quote these orders.</p>@endif
|
||||||
@foreach([['weight','Weight (kg)','0.1'],['length','Length (cm)','1'],['width','Width (cm)','1'],['height','Height (cm)','1'],['value','Parcel value (£)','0.01']] as [$field,$label,$step])<label class="grid gap-2 text-sm">{{ $label }}<input type="number" name="{{ $field }}" value="{{ old($field) }}" min="0" step="{{ $step }}" required class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3"></label>@endforeach
|
<div class="mt-5 grid gap-4">
|
||||||
<div class="flex items-end"><button class="w-full rounded-xl bg-brand px-5 py-3 font-semibold text-slate-950">Compare services</button></div>
|
@forelse($pendingOrders as $receipt)
|
||||||
</form></section>
|
@php $canQuote = filled($collectionPostcode) && filled($receipt['zip'] ?? null) && ($receipt['country_iso'] ?? 'GB') === 'GB' && ($receipt['grandtotal']['currency_code'] ?? 'GBP') === 'GBP'; @endphp
|
||||||
@if(count($quotes))<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><h3 class="text-lg font-semibold">2. Choose a collection service</h3><div class="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">@foreach($quotes as $quote)@php $service = $quote['Service'] ?? []; @endphp<article class="rounded-xl border border-slate-700 bg-slate-950 p-4"><p class="font-semibold">{{ $service['Name'] ?? 'Courier service' }}</p><p class="mt-1 text-sm text-slate-400">{{ $service['CourierName'] ?? '' }} · {{ $service['CollectionType'] ?? 'Unknown' }}</p><p class="mt-3 text-2xl font-bold text-brand">{{ isset($quote['TotalPrice']) ? '£'.number_format((float) $quote['TotalPrice'], 2) : 'See details' }}</p>@if(($service['CollectionType'] ?? null) === 'Collection' && ! empty($service['Slug']))<form method="post" action="{{ route('shipping.collection-dates') }}" class="mt-4 grid gap-3">@csrf<input type="hidden" name="service" value="{{ $service['Slug'] }}"><label class="grid gap-1 text-xs text-slate-400">Collection street<input name="sender_street" value="{{ old('sender_street') }}" required class="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white"></label><label class="grid gap-1 text-xs text-slate-400">Collection town<input name="sender_town" value="{{ old('sender_town') }}" required class="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white"></label><button class="rounded-lg border border-brand/40 px-3 py-2 text-sm font-semibold text-brand">Check collection dates</button></form>@endif</article>@endforeach</div></section>@endif
|
<article class="rounded-xl border border-slate-700 bg-slate-950 p-4">
|
||||||
@if(count($collectionDates) && $selectedService)<section class="rounded-2xl border border-brand/30 bg-slate-900 p-6"><h3 class="text-lg font-semibold">3. Arrange collection</h3><p class="mt-1 text-sm text-slate-400">Creating the order does not take payment. Parcel2Go will provide a payment link.</p><form method="post" action="{{ route('shipping.book-collection') }}" class="mt-5 grid gap-5">@csrf<label class="grid max-w-md gap-2 text-sm">Collection date<select name="collection_date" required class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3"><option value="">Choose a date</option>@foreach($collectionDates as $date)<option value="{{ $date['CollectionDate'] ?? '' }}">{{ isset($date['CollectionDate']) ? \Illuminate\Support\Carbon::parse($date['CollectionDate'])->format('D d M Y') : 'Available date' }}{{ ! empty($date['Surcharge']) ? ' · £'.number_format((float) $date['Surcharge'], 2).' surcharge' : '' }}</option>@endforeach</select></label>
|
<div class="flex flex-wrap items-start justify-between gap-3"><div><p class="font-semibold">Order #{{ $receipt['receipt_id'] ?? '—' }}</p><p class="mt-1 text-sm text-slate-300">{{ $receipt['name'] ?? 'Buyer' }} · {{ $receipt['city'] ?? 'City unavailable' }} · {{ $receipt['zip'] ?? 'Postcode unavailable' }}</p></div>@if(isset($receipt['grandtotal']['amount']))<p class="text-sm font-semibold text-brand">{{ $receipt['grandtotal']['currency_code'] ?? 'GBP' }} {{ number_format((float) $receipt['grandtotal']['amount'] / max(1, (int) ($receipt['grandtotal']['divisor'] ?? 1)), 2) }}</p>@endif</div>
|
||||||
<div class="grid gap-5 lg:grid-cols-2"><fieldset class="grid gap-3 rounded-xl border border-slate-700 p-4"><legend class="px-2 font-semibold">Sender and collection</legend>@foreach([['sender_name','Name'],['sender_email','Email'],['sender_phone','Phone'],['sender_property','Property'],['sender_street','Street'],['sender_town','Town']] as [$field,$label])<label class="grid gap-1 text-sm">{{ $label }}<input name="{{ $field }}" value="{{ old($field, session('shipping_sender_location.'.$field)) }}" required class="rounded-lg border border-slate-700 bg-slate-950 px-3 py-2"></label>@endforeach</fieldset><fieldset class="grid gap-3 rounded-xl border border-slate-700 p-4"><legend class="px-2 font-semibold">Recipient and delivery</legend>@foreach([['recipient_name','Name'],['recipient_email','Email'],['recipient_phone','Phone'],['recipient_property','Property'],['recipient_street','Street'],['recipient_town','Town']] as [$field,$label])<label class="grid gap-1 text-sm">{{ $label }}<input name="{{ $field }}" value="{{ old($field) }}" required class="rounded-lg border border-slate-700 bg-slate-950 px-3 py-2"></label>@endforeach</fieldset></div><label class="grid max-w-xl gap-2 text-sm">Parcel contents<input name="contents" value="{{ old('contents') }}" required maxlength="200" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3"></label><button class="w-fit rounded-xl bg-brand px-5 py-3 font-semibold text-slate-950">Create unpaid order</button></form></section>@endif
|
@if($canQuote)
|
||||||
<section class="overflow-x-auto rounded-2xl border border-slate-800 bg-slate-900"><h3 class="p-6 text-lg font-semibold">Recent shipments and labels</h3><table class="w-full min-w-[750px] text-left text-sm"><thead class="border-y border-slate-800 text-slate-400"><tr><th class="p-4">Order</th><th class="p-4">Courier</th><th class="p-4">Service</th><th class="p-4">Status</th><th class="p-4">Recipient</th><th class="p-4">Label</th></tr></thead><tbody class="divide-y divide-slate-800">@forelse(($orders['Orders'] ?? []) as $order)<tr><td class="p-4">{{ $order['OrderLineId'] ?? '—' }}</td><td class="p-4">{{ $order['Courier'] ?? '—' }}</td><td class="p-4">{{ $order['Service'] ?? '—' }}</td><td class="p-4">{{ ($order['Tracking']['Delivered'] ?? false) ? 'Delivered' : 'In transit' }}</td><td class="p-4">{{ $order['DeliveryAddress']['ContactName'] ?? '—' }}</td><td class="p-4">@if(! empty($order['Hash']) && ! empty($order['OrderLineId']))<a class="font-semibold text-brand hover:underline" href="{{ route('shipping.label', $order['OrderLineId']) }}">Download PDF</a>@else<span class="text-slate-500">Unavailable</span>@endif</td></tr>@empty<tr><td colspan="6" class="p-6 text-slate-400">{{ $profileAvailable ? 'No shipments available.' : 'Account history requires my-profile access from Parcel2Go.' }}</td></tr>@endforelse</tbody></table></section>
|
<form method="post" action="{{ route('shipping.quote') }}" class="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-5">@csrf<input type="hidden" name="receipt_id" value="{{ $receipt['receipt_id'] }}">
|
||||||
@endsection
|
@foreach([['weight','Weight (kg)','0.1'],['length','Length (cm)','1'],['width','Width (cm)','1'],['height','Height (cm)','1']] as [$field,$label,$step])<label class="grid gap-1 text-xs text-slate-400">{{ $label }}<input type="number" name="{{ $field }}" value="{{ old($field) }}" min="0.01" step="{{ $step }}" required class="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white"></label>@endforeach
|
||||||
|
<div class="flex items-end"><button class="w-full rounded-lg bg-brand px-4 py-2 text-sm font-semibold text-slate-950">Quote this order</button></div>
|
||||||
|
</form>
|
||||||
|
@else<p class="mt-3 text-sm text-amber-200">{{ blank($collectionPostcode) ? 'Add a collection postcode in Settings.' : 'A UK delivery postcode and GBP order value are needed from Etsy before this order can be quoted.' }}</p>@endif
|
||||||
|
</article>
|
||||||
|
@empty<p class="rounded-xl border border-slate-700 bg-slate-950 p-5 text-sm text-slate-400">No paid, unshipped Etsy orders to prepare.</p>@endforelse
|
||||||
|
</div>
|
||||||
|
</section>@if(count($quotes) && $selectedReceipt)<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><h3 class="text-lg font-semibold">Collection services for Etsy order #{{ $selectedReceipt['receipt_id'] }}</h3><div class="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">@foreach($quotes as $quote)@php $service = $quote['Service'] ?? []; @endphp<article class="rounded-xl border border-slate-700 bg-slate-950 p-4"><p class="font-semibold">{{ $service['Name'] ?? 'Courier service' }}</p><p class="mt-1 text-sm text-slate-400">{{ $service['CourierName'] ?? '' }} · {{ $service['CollectionType'] ?? 'Unknown' }}</p><p class="mt-3 text-2xl font-bold text-brand">{{ isset($quote['TotalPrice']) ? '£'.number_format((float) $quote['TotalPrice'], 2) : 'See details' }}</p>@if(($service['CollectionType'] ?? null) === 'Collection' && ! empty($service['Slug']))<form method="post" action="{{ route('shipping.collection-dates') }}" class="mt-4 grid gap-3">@csrf<input type="hidden" name="service" value="{{ $service['Slug'] }}"><label class="grid gap-1 text-xs text-slate-400">Collection street<input name="sender_street" value="{{ old('sender_street') }}" required class="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white"></label><label class="grid gap-1 text-xs text-slate-400">Collection town<input name="sender_town" value="{{ old('sender_town') }}" required class="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white"></label><button class="rounded-lg border border-brand/40 px-3 py-2 text-sm font-semibold text-brand">Check collection dates</button></form>@endif</article>@endforeach</div></section>@endif
|
||||||
|
@if(count($collectionDates) && $selectedService && $selectedReceipt)<section class="rounded-2xl border border-brand/30 bg-slate-900 p-6"><h3 class="text-lg font-semibold">Arrange collection for Etsy order #{{ $selectedReceipt['receipt_id'] }}</h3><p class="mt-1 text-sm text-slate-400">Creating the order does not take payment. Parcel2Go will provide a payment link.</p><form method="post" action="{{ route('shipping.book-collection') }}" class="mt-5 grid gap-5">@csrf<label class="grid max-w-md gap-2 text-sm">Collection date<select name="collection_date" required class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3"><option value="">Choose a date</option>@foreach($collectionDates as $date)<option value="{{ $date['CollectionDate'] ?? '' }}">{{ isset($date['CollectionDate']) ? \Illuminate\Support\Carbon::parse($date['CollectionDate'])->format('D d M Y') : 'Available date' }}{{ ! empty($date['Surcharge']) ? ' · £'.number_format((float) $date['Surcharge'], 2).' surcharge' : '' }}</option>@endforeach</select></label>
|
||||||
|
<div class="grid gap-5 lg:grid-cols-2"><fieldset class="grid gap-3 rounded-xl border border-slate-700 p-4"><legend class="px-2 font-semibold">Sender and collection</legend>@foreach([['sender_name','Name'],['sender_email','Email'],['sender_phone','Phone'],['sender_property','Property'],['sender_street','Street'],['sender_town','Town']] as [$field,$label])<label class="grid gap-1 text-sm">{{ $label }}<input name="{{ $field }}" value="{{ old($field, session('shipping_sender_location.'.$field)) }}" required class="rounded-lg border border-slate-700 bg-slate-950 px-3 py-2"></label>@endforeach</fieldset><fieldset class="grid gap-3 rounded-xl border border-slate-700 p-4"><legend class="px-2 font-semibold">Recipient and delivery</legend>@foreach([['recipient_name','Name'],['recipient_email','Email'],['recipient_phone','Phone'],['recipient_property','Property'],['recipient_street','Street'],['recipient_town','Town']] as [$field,$label])<label class="grid gap-1 text-sm">{{ $label }}<input name="{{ $field }}" value="{{ old($field, ['recipient_name' => $selectedReceipt['name'] ?? '', 'recipient_email' => $selectedReceipt['buyer_email'] ?? '', 'recipient_property' => $selectedReceipt['first_line'] ?? '', 'recipient_street' => $selectedReceipt['second_line'] ?? '', 'recipient_town' => $selectedReceipt['city'] ?? ''][$field] ?? '') }}" required class="rounded-lg border border-slate-700 bg-slate-950 px-3 py-2"></label>@endforeach</fieldset></div><label class="grid max-w-xl gap-2 text-sm">Parcel contents<input name="contents" value="{{ old('contents') }}" required maxlength="200" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3"></label><button class="w-fit rounded-xl bg-brand px-5 py-3 font-semibold text-slate-950">Create unpaid order</button></form></section>@endif
|
||||||
|
<section class="overflow-x-auto rounded-2xl border border-slate-800 bg-slate-900">
|
||||||
|
<div class="p-6"><h3 class="text-lg font-semibold">Existing shipments</h3><p class="mt-1 text-sm text-slate-400">Current Parcel2Go progress and labels from your account history.</p></div>
|
||||||
|
<table class="w-full min-w-[800px] text-left text-sm"><thead class="border-y border-slate-800 text-slate-400"><tr><th class="p-4">Order</th><th class="p-4">Courier</th><th class="p-4">Service</th><th class="p-4">Progress</th><th class="p-4">Estimated delivery</th><th class="p-4">Recipient</th><th class="p-4">Label</th></tr></thead><tbody class="divide-y divide-slate-800">
|
||||||
|
@forelse(($orders['Orders'] ?? []) as $order)
|
||||||
|
<tr><td class="p-4">{{ $order['OrderLineId'] ?? '—' }}</td><td class="p-4">{{ $order['Courier'] ?? '—' }}</td><td class="p-4">{{ $order['Service'] ?? '—' }}</td><td class="p-4"><span @class(['rounded-full px-2 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $order['Tracking']['Delivered'] ?? false, 'bg-sky-500/10 text-sky-300' => ! ($order['Tracking']['Delivered'] ?? false)])>{{ ($order['Tracking']['Delivered'] ?? false) ? 'Delivered' : 'In transit' }}</span></td><td class="p-4">{{ $order['EstimatedDeliveryDate'] ?? '—' }}</td><td class="p-4">{{ $order['DeliveryAddress']['ContactName'] ?? '—' }}</td><td class="p-4">@if(! empty($order['Hash']) && ! empty($order['OrderLineId']))<a class="font-semibold text-brand hover:underline" href="{{ route('shipping.label', $order['OrderLineId']) }}">Download PDF</a>@else<span class="text-slate-500">Unavailable</span>@endif</td></tr>
|
||||||
|
@empty<tr><td colspan="7" class="p-6 text-slate-400">{{ $historyAvailable ? 'No shipments in Parcel2Go account history.' : 'Shipment history is unavailable until Parcel2Go grants access to this client.' }}</td></tr>@endforelse
|
||||||
|
</tbody></table>
|
||||||
|
</section>@endsection
|
||||||
|
|||||||
@@ -108,8 +108,9 @@ class IntegrationSettingsTest extends TestCase
|
|||||||
$this->assertDatabaseCount('integration_settings', 0);
|
$this->assertDatabaseCount('integration_settings', 0);
|
||||||
|
|
||||||
$this->actingAs($admin)->put(route('settings.integrations.update', 'parcel2go'), [
|
$this->actingAs($admin)->put(route('settings.integrations.update', 'parcel2go'), [
|
||||||
'scope' => 'public-api my-profile',
|
'scope' => 'public-api my-profile', 'collection_postcode' => 'SW1A 1AA',
|
||||||
])->assertRedirect(route('settings.index'));
|
])->assertRedirect(route('settings.index'));
|
||||||
$this->assertSame('public-api my-profile', IntegrationSetting::findOrFail('parcel2go')->settings['scope']);
|
$this->assertSame('public-api my-profile', IntegrationSetting::findOrFail('parcel2go')->settings['scope']);
|
||||||
|
$this->assertSame('SW1A 1AA', IntegrationSetting::findOrFail('parcel2go')->settings['collection_postcode']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Models\EtsyConnection;
|
use App\Models\EtsyConnection;
|
||||||
|
use App\Models\IntegrationSetting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
@@ -65,6 +66,7 @@ class IntegrationsTest extends TestCase
|
|||||||
|
|
||||||
public function test_shipping_quote_sends_parcel_to_parcel2go(): void
|
public function test_shipping_quote_sends_parcel_to_parcel2go(): void
|
||||||
{
|
{
|
||||||
|
$this->pendingEtsyOrder();
|
||||||
Http::preventStrayRequests();
|
Http::preventStrayRequests();
|
||||||
Http::fake([
|
Http::fake([
|
||||||
'https://auth.example.test/token' => Http::response(['access_token' => 'test-token']),
|
'https://auth.example.test/token' => Http::response(['access_token' => 'test-token']),
|
||||||
@@ -75,11 +77,11 @@ class IntegrationsTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
config()->set('services.parcel2go', ['client_id' => 'id', 'client_secret' => 'secret', 'auth_url' => 'https://auth.example.test/token', 'base_url' => 'https://shipping.example.test/api']);
|
config()->set('services.parcel2go', ['client_id' => 'id', 'client_secret' => 'secret', 'auth_url' => 'https://auth.example.test/token', 'base_url' => 'https://shipping.example.test/api']);
|
||||||
$admin = User::factory()->create(['role' => 'admin']);
|
$admin = User::factory()->create(['role' => 'admin']);
|
||||||
$details = ['collection_postcode' => 'SW1A 1AA', 'delivery_postcode' => 'M1 1AE', 'weight' => 2, 'length' => 10, 'width' => 10, 'height' => 10, 'value' => 30];
|
$details = ['receipt_id' => 77, 'delivery_postcode' => 'SW1A 1AA', 'weight' => 2, 'length' => 10, 'width' => 10, 'height' => 10];
|
||||||
|
|
||||||
$this->actingAs($admin)->post(route('shipping.quote'), $details)->assertRedirect(route('shipping'));
|
$this->actingAs($admin)->post(route('shipping.quote'), $details)->assertRedirect(route('shipping'));
|
||||||
$this->actingAs($admin)->get(route('shipping'))->assertOk()->assertSee('Evri')->assertSee('£5.50');
|
$this->actingAs($admin)->get(route('shipping'))->assertOk()->assertSee('Evri')->assertSee('£5.50')->assertDontSee('name="delivery_postcode"', false);
|
||||||
Http::assertSent(fn ($request) => $request->url() === 'https://shipping.example.test/api/quotes' && $request['Parcels'][0]['Weight'] === 2);
|
Http::assertSent(fn ($request) => $request->url() === 'https://shipping.example.test/api/quotes' && $request['Parcels'][0]['Weight'] === 2 && $request['DeliveryAddress']['Postcode'] === 'M1 1AE' && $request['CollectionAddress']['Postcode'] === 'SW1A 1AA');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_shipping_page_works_with_public_api_scope_without_profile_access(): void
|
public function test_shipping_page_works_with_public_api_scope_without_profile_access(): void
|
||||||
@@ -89,7 +91,7 @@ class IntegrationsTest extends TestCase
|
|||||||
return $request['scope'] === 'public-api'
|
return $request['scope'] === 'public-api'
|
||||||
? Http::response(['access_token' => 'test-token'])
|
? Http::response(['access_token' => 'test-token'])
|
||||||
: Http::response(['error' => 'invalid_scope'], 400);
|
: Http::response(['error' => 'invalid_scope'], 400);
|
||||||
}]);
|
}, 'https://shipping.example.test/api/me/orders/detail' => Http::response([], 403)]);
|
||||||
config()->set('services.parcel2go', [
|
config()->set('services.parcel2go', [
|
||||||
'client_id' => 'id', 'client_secret' => 'secret',
|
'client_id' => 'id', 'client_secret' => 'secret',
|
||||||
'auth_url' => 'https://auth.example.test/token',
|
'auth_url' => 'https://auth.example.test/token',
|
||||||
@@ -98,8 +100,45 @@ class IntegrationsTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create(['role' => 'admin']))->get(route('shipping'))
|
$this->actingAs(User::factory()->create(['role' => 'admin']))->get(route('shipping'))
|
||||||
->assertOk()->assertSee('Parcel2Go connected for quotes and collections')
|
->assertOk()->assertSee('Pending Etsy orders')->assertSee('Parcel2Go denied shipment history')
|
||||||
->assertDontSee('Parcel2Go is unavailable');
|
->assertDontSee('Parcel2Go is unavailable');
|
||||||
|
Http::assertSentCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_shipping_shows_existing_shipments_when_basic_access_can_read_history(): void
|
||||||
|
{
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
Http::fake([
|
||||||
|
'https://auth.example.test/token' => Http::response(['access_token' => 'test-token']),
|
||||||
|
'https://shipping.example.test/api/me/orders/detail' => Http::response(['Orders' => [[
|
||||||
|
'OrderLineId' => 123, 'Courier' => 'Evri', 'Service' => 'Standard',
|
||||||
|
'Tracking' => ['Delivered' => true], 'DeliveryAddress' => ['ContactName' => 'Buyer'],
|
||||||
|
'EstimatedDeliveryDate' => '2026-10-15', 'Hash' => 'hash',
|
||||||
|
]]]),
|
||||||
|
]);
|
||||||
|
config()->set('services.parcel2go', [
|
||||||
|
'client_id' => 'id', 'client_secret' => 'secret',
|
||||||
|
'auth_url' => 'https://auth.example.test/token',
|
||||||
|
'base_url' => 'https://shipping.example.test/api',
|
||||||
|
'scope' => 'public-api',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->create(['role' => 'admin']))->get(route('shipping'))
|
||||||
|
->assertOk()->assertSee('Existing shipments')->assertSee('Delivered')->assertSee('Buyer')
|
||||||
|
->assertDontSee('Parcel2Go denied shipment history');
|
||||||
|
Http::assertSentCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_shipping_rejects_a_quote_for_an_order_that_is_not_pending(): void
|
||||||
|
{
|
||||||
|
$this->pendingEtsyOrder();
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
$admin = User::factory()->create(['role' => 'admin']);
|
||||||
|
|
||||||
|
$this->actingAs($admin)->post(route('shipping.quote'), [
|
||||||
|
'receipt_id' => 999, 'weight' => 2, 'length' => 10, 'width' => 10, 'height' => 10,
|
||||||
|
'delivery_postcode' => 'SW1A 1AA',
|
||||||
|
])->assertRedirect(route('shipping'))->assertSessionHasErrors('quote');
|
||||||
Http::assertSentCount(1);
|
Http::assertSentCount(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +149,7 @@ class IntegrationsTest extends TestCase
|
|||||||
return $request['scope'] === 'public-api'
|
return $request['scope'] === 'public-api'
|
||||||
? Http::response(['access_token' => 'test-token'])
|
? Http::response(['access_token' => 'test-token'])
|
||||||
: Http::response(['error' => 'invalid_scope'], 400);
|
: Http::response(['error' => 'invalid_scope'], 400);
|
||||||
}]);
|
}, 'https://shipping.example.test/api/me/orders/detail' => Http::response([], 403)]);
|
||||||
config()->set('services.parcel2go', [
|
config()->set('services.parcel2go', [
|
||||||
'client_id' => 'id', 'client_secret' => 'secret',
|
'client_id' => 'id', 'client_secret' => 'secret',
|
||||||
'auth_url' => 'https://auth.example.test/token',
|
'auth_url' => 'https://auth.example.test/token',
|
||||||
@@ -119,10 +158,9 @@ class IntegrationsTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create(['role' => 'admin']))->get(route('shipping'))
|
$this->actingAs(User::factory()->create(['role' => 'admin']))->get(route('shipping'))
|
||||||
->assertOk()->assertSee('Parcel2Go rejected the selected API access')
|
->assertOk()->assertSee('Parcel2Go denied shipment history')
|
||||||
->assertDontSee('Parcel2Go connected, but account history is unavailable')
|
|
||||||
->assertDontSee('Parcel2Go is unavailable');
|
->assertDontSee('Parcel2Go is unavailable');
|
||||||
Http::assertSentCount(2);
|
Http::assertSentCount(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_shipping_label_is_downloaded_for_an_order_in_account_history(): void
|
public function test_shipping_label_is_downloaded_for_an_order_in_account_history(): void
|
||||||
@@ -142,6 +180,7 @@ class IntegrationsTest extends TestCase
|
|||||||
|
|
||||||
public function test_collection_booking_uses_quoted_service_and_available_date(): void
|
public function test_collection_booking_uses_quoted_service_and_available_date(): void
|
||||||
{
|
{
|
||||||
|
$this->pendingEtsyOrder();
|
||||||
Http::preventStrayRequests();
|
Http::preventStrayRequests();
|
||||||
Http::fake([
|
Http::fake([
|
||||||
'https://auth.example.test/token' => Http::response(['access_token' => 'test-token']),
|
'https://auth.example.test/token' => Http::response(['access_token' => 'test-token']),
|
||||||
@@ -153,6 +192,7 @@ class IntegrationsTest extends TestCase
|
|||||||
config()->set('services.parcel2go', ['client_id' => 'id', 'client_secret' => 'secret', 'auth_url' => 'https://auth.example.test/token', 'base_url' => 'https://shipping.example.test/api']);
|
config()->set('services.parcel2go', ['client_id' => 'id', 'client_secret' => 'secret', 'auth_url' => 'https://auth.example.test/token', 'base_url' => 'https://shipping.example.test/api']);
|
||||||
$session = [
|
$session = [
|
||||||
'shipping_quote' => ['collection_postcode' => 'SW1A 1AA', 'delivery_postcode' => 'M1 1AE', 'weight' => 2, 'length' => 10, 'width' => 10, 'height' => 10, 'value' => 30],
|
'shipping_quote' => ['collection_postcode' => 'SW1A 1AA', 'delivery_postcode' => 'M1 1AE', 'weight' => 2, 'length' => 10, 'width' => 10, 'height' => 10, 'value' => 30],
|
||||||
|
'shipping_receipt_id' => 77,
|
||||||
'shipping_selected_service' => 'evri-collection',
|
'shipping_selected_service' => 'evri-collection',
|
||||||
'shipping_quotes' => [['Service' => ['Slug' => 'evri-collection', 'CollectionType' => 'Collection']]],
|
'shipping_quotes' => [['Service' => ['Slug' => 'evri-collection', 'CollectionType' => 'Collection']]],
|
||||||
'shipping_collection_dates' => [['CollectionDate' => '2026-10-15T10:00:00Z']],
|
'shipping_collection_dates' => [['CollectionDate' => '2026-10-15T10:00:00Z']],
|
||||||
@@ -249,4 +289,24 @@ class IntegrationsTest extends TestCase
|
|||||||
])->assertUnprocessable()->assertJsonValidationErrors('targetTemperature');
|
])->assertUnprocessable()->assertJsonValidationErrors('targetTemperature');
|
||||||
Http::assertNothingSent();
|
Http::assertNothingSent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function pendingEtsyOrder(): void
|
||||||
|
{
|
||||||
|
config()->set('services.etsy', ['keystring' => 'key', 'shared_secret' => 'secret']);
|
||||||
|
EtsyConnection::create([
|
||||||
|
'shop_id' => 42, 'shop_name' => 'HickLab3D',
|
||||||
|
'access_token' => '123.token', 'refresh_token' => '123.refresh',
|
||||||
|
'expires_at' => now()->addHour(),
|
||||||
|
]);
|
||||||
|
IntegrationSetting::create([
|
||||||
|
'provider' => 'parcel2go',
|
||||||
|
'settings' => ['collection_postcode' => 'SW1A 1AA'],
|
||||||
|
]);
|
||||||
|
Http::fake([
|
||||||
|
'https://api.etsy.com/v3/application/shops/42/receipts*' => Http::response(['results' => [[
|
||||||
|
'receipt_id' => 77, 'name' => 'Buyer', 'zip' => 'M1 1AE', 'city' => 'Manchester', 'country_iso' => 'GB',
|
||||||
|
'grandtotal' => ['amount' => 3000, 'divisor' => 100, 'currency_code' => 'GBP'],
|
||||||
|
]]]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user