Files
twotalesanimation 2a10f9af38 feat: Complete Shiplogic integration with mobile-optimized ops workflow
**Shiplogic API Integration:**
- Fixed API base URL configuration (removed /api suffix)
- Implemented comprehensive request/response logging for rates and shipments endpoints
- Fixed PDF fetching: API returns S3 URLs, now downloads actual PDFs from S3
- Added tests and mock API responses for local development (routes/shiplogic-mock.php)

**Courier Service Enhancements:**
- Added redownloadShipmentPdfs() public method for re-downloading corrupted PDFs
- Enhanced error logging with full request/response bodies for debugging
- Proper binary PDF storage using Laravel Storage facade
- URL and S3 download handling for Shiplogic API responses

**Workflow & Operations:**
- Changed to manual "Ready for Collection" button instead of automatic move
- Operators now: scan QR → apply labels → click "Ready for Collection" → moves to Awaiting Collection
- Removed duplicate PDF attachments to Trello (was adding twice from two listeners)
- Fixed NotifySlackOnShipmentCreated to only handle Slack notifications

**Mobile-Optimized Ops Page:**
- Removed QR code display from order detail page
- Implemented responsive single-column layout for mobile phones
- Large touch-friendly buttons (full width, increased padding)
- Bold typography for better readability on small screens
- Larger input fields and tracking number displays
- Clear step-by-step instructions for warehouse operators
- Re-download PDF button for damaged/corrupted labels

**New Features:**
- POST /ops/orders/{uuid}/ready-for-collection endpoint
- Re-download PDFs functionality accessible from awaiting_collection and in_transit states
- Full audit logging for all operations via ops interface
- Proper error handling and user feedback

**Testing:**
- Added ShipmentCreationTest with mock HTTP client
- Created comprehensive testing guide (SHIPLOGIC_TESTING.md)
- Mock API routes for local development without hitting live API
2026-01-03 16:13:20 +02:00

160 lines
8.6 KiB
PHP

<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\WallpapersController;
use App\Http\Controllers\MuralsController;
use App\Http\Controllers\FabricsController;
use App\Http\Controllers\ProductController;
use App\Http\Controllers\CartController;
use App\Http\Controllers\OrderController;
use App\Http\Controllers\CustomOrderController;
use App\Http\Controllers\Auth\GoogleAuthController;
use App\Http\Controllers\OpsController;
use App\Http\Controllers\PackingController;
use App\Http\Controllers\ShippingController;
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/wallpapers', [WallpapersController::class, 'index'])->name('wallpapers');
Route::get('/murals', [MuralsController::class, 'index'])->name('murals');
Route::get('/fabrics', [FabricsController::class, 'index'])->name('fabrics');
// Product routes
Route::get('/products/{product}', [ProductController::class, 'show'])->name('product-detail');
// Authentication routes
Route::get('/login', function () { return view('auth.login'); })->name('auth.login');
Route::get('/auth/google', [GoogleAuthController::class, 'redirect'])->name('auth.google');
Route::get('/auth/google/callback', [GoogleAuthController::class, 'callback'])->name('auth.google.callback');
Route::post('/logout', [GoogleAuthController::class, 'logout'])->name('logout');
// Cart routes
Route::get('/cart', [CartController::class, 'index'])->name('cart');
Route::post('/cart/add/{product}', [CartController::class, 'add'])->name('cart-add');
Route::post('/cart/sample/{product}', [CartController::class, 'orderSample'])->name('order-sample');
Route::post('/cart/update', [CartController::class, 'update'])->name('cart-update');
Route::post('/cart/remove', [CartController::class, 'remove'])->name('cart-remove');
Route::post('/cart/clear', [CartController::class, 'clear'])->name('cart-clear');
// Order routes
Route::get('/checkout', [OrderController::class, 'checkout'])->name('checkout');
Route::post('/orders/process', [OrderController::class, 'process'])->name('order-process');
Route::get('/orders/success/{order:uuid}', [OrderController::class, 'success'])->name('order-success');
Route::get('/orders/history', [OrderController::class, 'history'])->name('order-history');
// Guest order tracking routes
Route::get('/track-order', [OrderController::class, 'trackForm'])->name('track-order-form');
Route::post('/track-order', [OrderController::class, 'trackSearch'])->name('track-order-search');
// Yoco payment routes
Route::get('/payment/yoco/{order:uuid}', [OrderController::class, 'yocoPayment'])->name('yoco-payment');
Route::get('/payment/yoco/success/{order:uuid}', [OrderController::class, 'yocoSuccess'])->name('yoco-success');
Route::get('/payment/yoco/cancel/{order:uuid}', [OrderController::class, 'yocoCancel'])->name('yoco-cancel');
Route::get('/payment/yoco/failure/{order:uuid}', [OrderController::class, 'yocoFailure'])->name('yoco-failure');
// Protected routes - authenticated users only
Route::middleware('auth')->group(function () {
Route::get('/my-account', 'App\Http\Controllers\AccountController@show')->name('my-account');
Route::post('/my-account', 'App\Http\Controllers\AccountController@update')->name('my-account.update');
Route::get('/my-orders', 'App\Http\Controllers\AccountController@orders')->name('my-orders');
Route::get('/my-orders/{order:uuid}', 'App\Http\Controllers\AccountController@orderDetail')->name('my-orders.detail');
Route::get('/custom-orders', [CustomOrderController::class, 'index'])->name('custom-orders.index');
Route::get('/custom-orders/create', 'App\Http\Controllers\CustomOrderController@create')->name('custom-orders.create');
Route::post('/custom-orders', 'App\Http\Controllers\CustomOrderController@store')->name('custom-orders.store');
Route::get('/custom-orders/{customOrder:uuid}', 'App\Http\Controllers\CustomOrderController@show')->name('custom-orders.show');
Route::post('/custom-orders/{customOrder:uuid}/approve-proof', [CustomOrderController::class, 'approveProof'])->name('custom-orders.approve-proof');
Route::post('/custom-orders/{customOrder:uuid}/request-changes', [CustomOrderController::class, 'requestChanges'])->name('custom-orders.request-changes');
Route::post('/custom-orders/{customOrder:uuid}/pay-balance', [CustomOrderController::class, 'markBalancePaid'])->name('custom-orders.pay-balance');
Route::post('/payment/yoco/custom/deposit', [CustomOrderController::class, 'depositPayment'])->name('yoco-custom-deposit');
Route::get('/payment/yoco/custom/deposit/success/{customOrder:uuid}', [CustomOrderController::class, 'depositSuccess'])->name('yoco-custom-deposit-success');
// Packing & Shipping routes (ops staff)
Route::post('/orders/{order:uuid}/pack', [PackingController::class, 'confirmPacked'])->name('orders.pack');
Route::post('/custom-orders/{customOrder:uuid}/pack', [PackingController::class, 'confirmPacked'])->name('custom-orders.pack');
Route::post('/orders/{order:uuid}/ship', [ShippingController::class, 'createShipment'])->name('orders.ship');
// QR-based operations (ops interface)
Route::post('/ops/orders/{order:uuid}/pack', [OpsController::class, 'confirmPacking'])->name('ops.order.pack');
Route::post('/ops/orders/{order:uuid}/inspection-passed', [OpsController::class, 'markInspectionPassed'])->name('ops.order.inspection-passed');
Route::post('/ops/orders/{order:uuid}/inspection-failed', [OpsController::class, 'flagInspectionIssue'])->name('ops.order.inspection-failed');
Route::get('/ops/orders/{order:uuid}/sticker/download', [OpsController::class, 'downloadSticker'])->name('ops.order.sticker.download');
Route::post('/ops/orders/{order:uuid}/redownload-pdfs', [OpsController::class, 'redownloadShipmentPdfs'])->name('ops.order.redownload-pdfs');
Route::post('/ops/orders/{order:uuid}/ready-for-collection', [OpsController::class, 'markReadyForCollection'])->name('ops.order.ready-for-collection');
Route::post('/custom-orders/{customOrder:uuid}/ship', [ShippingController::class, 'createShipment'])->name('custom-orders.ship');
});
// QR landing page (public but token-gated)
Route::get('/ops/orders/{token}', [OpsController::class, 'showOrder'])->name('ops.order.show');
// Test routes (remove in production)
Route::get('/test/sticker/{orderNumber}', function ($orderNumber) {
$order = \App\Models\Order::where('order_number', $orderNumber)->first();
if (!$order) {
return response()->json(['error' => 'Order not found'], 404);
}
$stickerPath = "qr-stickers/{$order->uuid}.pdf";
if (!\Illuminate\Support\Facades\Storage::disk('public')->exists($stickerPath)) {
return response()->json([
'error' => 'Sticker PDF not generated yet',
'order_uuid' => $order->uuid,
'regenerate_url' => route('test.sticker.regenerate', ['orderNumber' => $orderNumber])
], 404);
}
$pdfUrl = \Illuminate\Support\Facades\Storage::disk('public')->url($stickerPath);
return view('test.sticker-display', [
'order' => $order,
'pdfUrl' => $pdfUrl,
'stickerPath' => $stickerPath,
]);
})->name('test.sticker');
// Regenerate sticker PDF for testing
Route::match(['get', 'post'], '/test/sticker/{orderNumber}/regenerate', function ($orderNumber) {
$order = \App\Models\Order::where('order_number', $orderNumber)->first();
if (!$order) {
return response('Order not found', 404);
}
try {
// Ensure order has QR token
if (!$order->qr_token) {
$order->qr_token = \Illuminate\Support\Str::random(32);
$order->qr_generated_at = now();
$order->save();
}
// Generate sticker
$service = new \App\Services\QrStickerService();
$paths = $service->generateSticker($order);
// Return PDF inline for viewing
$pdfPath = storage_path('app/public/' . $paths['pdf_path']);
return response()->file($pdfPath, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="QR-' . $order->order_number . '.pdf"'
]);
} catch (\Exception $e) {
return response('Failed to generate sticker: ' . $e->getMessage(), 500);
}
})->name('test.sticker.regenerate');
// use Illuminate\Support\Facades\Route;
Route::get('/font-test', function () {
dd(
file_exists(storage_path('fonts/AbrilFatface-Regular.ttf')),
is_readable(storage_path('fonts/AbrilFatface-Regular.ttf'))
);
});
// Test route for invoice generation (remove in production)
require __DIR__ . '/test-invoice.php';