diff --git a/app/Http/Controllers/CustomOrderController.php b/app/Http/Controllers/CustomOrderController.php index 5b052a0..0e96dc8 100644 --- a/app/Http/Controllers/CustomOrderController.php +++ b/app/Http/Controllers/CustomOrderController.php @@ -5,8 +5,14 @@ namespace App\Http\Controllers; use App\Models\CustomOrder; use App\Models\CustomOrderFile; use App\Models\CustomOrderSpecification; +use App\Models\CustomOrderProof; use App\Models\AppSetting; use App\Models\PrintStock; +use App\Events\OrderCreated; +use App\Events\DepositPaid; +use App\Events\ProofApproved; +use App\Events\ProofRevisionRequested; +use App\Events\BalancePaid; use Illuminate\Http\Request; use Illuminate\View\View; use Illuminate\Support\Facades\Storage; @@ -141,6 +147,9 @@ class CustomOrderController extends Controller } } + // Emit events for custom order creation with deposit + OrderCreated::dispatch($customOrder, 'custom'); + return redirect()->route('custom-orders.show', $customOrder)->with('success', 'Custom order created successfully. Please review the quote and proceed with deposit payment.'); } @@ -411,4 +420,147 @@ class CustomOrderController extends Controller return null; } } + + /** + * Approve proof for custom order + * + * POST /custom-orders/{id}/approve-proof + */ + public function approveProof(Request $request, CustomOrder $customOrder) + { + // Authorization: only allow owner or admin + if (auth()->user()->id !== $customOrder->user_id && !auth()->user()->is_admin) { + abort(403, 'Unauthorized'); + } + + try { + $customOrder->update([ + 'proof_approved' => true, + 'proof_approved_at' => now(), + ]); + + Log::info('Proof approved for custom order', [ + 'custom_order_id' => $customOrder->id, + 'approved_by' => auth()->id(), + ]); + + // Emit event + ProofApproved::dispatch($customOrder); + + return response()->json([ + 'success' => true, + 'message' => 'Proof approved successfully', + 'proof_approved_at' => $customOrder->proof_approved_at, + ]); + } catch (\Exception $e) { + Log::error('Failed to approve proof', [ + 'custom_order_id' => $customOrder->id, + 'error' => $e->getMessage(), + ]); + + return response()->json([ + 'error' => 'Failed to approve proof', + 'message' => $e->getMessage(), + ], 500); + } + } + + /** + * Request proof revision for custom order + * + * POST /custom-orders/{id}/request-changes + */ + public function requestChanges(Request $request, CustomOrder $customOrder) + { + // Authorization: only allow owner or admin + if (auth()->user()->id !== $customOrder->user_id && !auth()->user()->is_admin) { + abort(403, 'Unauthorized'); + } + + $validated = $request->validate([ + 'revision_notes' => 'required|string|min:10|max:1000', + ]); + + try { + $customOrder->update([ + 'proof_approved' => false, + ]); + + Log::info('Proof revision requested for custom order', [ + 'custom_order_id' => $customOrder->id, + 'requested_by' => auth()->id(), + 'notes' => $validated['revision_notes'], + ]); + + // Emit event + ProofRevisionRequested::dispatch($customOrder, $validated['revision_notes']); + + return response()->json([ + 'success' => true, + 'message' => 'Revision request sent successfully', + ]); + } catch (\Exception $e) { + Log::error('Failed to request proof revision', [ + 'custom_order_id' => $customOrder->id, + 'error' => $e->getMessage(), + ]); + + return response()->json([ + 'error' => 'Failed to request revision', + 'message' => $e->getMessage(), + ], 500); + } + } + + /** + * Mark balance as paid for custom order + * + * POST /custom-orders/{id}/pay-balance + */ + public function markBalancePaid(Request $request, CustomOrder $customOrder) + { + // Authorization: only allow owner or admin + if (auth()->user()->id !== $customOrder->user_id && !auth()->user()->is_admin) { + abort(403, 'Unauthorized'); + } + + // Verify proof is approved before balance payment + if (!$customOrder->proof_approved) { + return response()->json([ + 'error' => 'Proof must be approved before balance payment', + 'proof_approved' => $customOrder->proof_approved, + ], 409); + } + + try { + $customOrder->update([ + 'balance_status' => 'paid', + 'status' => 'printing', + ]); + + Log::info('Balance paid for custom order', [ + 'custom_order_id' => $customOrder->id, + 'marked_by' => auth()->id(), + ]); + + // Emit event + BalancePaid::dispatch($customOrder, $customOrder->balance_amount); + + return response()->json([ + 'success' => true, + 'message' => 'Balance payment recorded successfully', + 'status' => $customOrder->status, + ]); + } catch (\Exception $e) { + Log::error('Failed to mark balance paid', [ + 'custom_order_id' => $customOrder->id, + 'error' => $e->getMessage(), + ]); + + return response()->json([ + 'error' => 'Failed to record balance payment', + 'message' => $e->getMessage(), + ], 500); + } + } } diff --git a/app/Http/Controllers/OrderController.php b/app/Http/Controllers/OrderController.php index 11b52f1..6b0e74e 100644 --- a/app/Http/Controllers/OrderController.php +++ b/app/Http/Controllers/OrderController.php @@ -10,6 +10,9 @@ use App\Models\PrintStock; use App\Services\ShippingService; use App\Services\InvoiceService; use App\Services\MailjetService; +use App\Events\OrderCreated; +use App\Events\DepositPaid; +use App\Events\BalancePaid; use Illuminate\Http\Request; class OrderController extends Controller @@ -562,7 +565,7 @@ class OrderController extends Controller if ($order->payment_status !== 'paid') { $order->update([ 'payment_status' => 'paid', - 'status' => 'processing', + 'status' => 'prep', 'yoco_checkout_response' => json_encode($payload), ]); @@ -609,6 +612,10 @@ class OrderController extends Controller 'error' => $e->getMessage(), ]); } + + // Emit OrderCreated and DepositPaid events (standard orders are fully paid upfront) + OrderCreated::dispatch($order, 'standard'); + DepositPaid::dispatch($order, $order->total); // Full payment is treated as deposit confirmation \Log::info('Yoco Webhook: Order updated for successful payment', [ 'order_uuid' => $orderUuid, @@ -645,10 +652,14 @@ class OrderController extends Controller if ($orderType === 'custom_deposit' && $order->deposit_status !== 'paid') { $order->update([ 'deposit_status' => 'paid', - 'status' => 'submitted', + 'status' => 'design', 'yoco_checkout_response' => json_encode($payload), ]); + // Emit events for custom order deposit + OrderCreated::dispatch($order, 'custom'); + DepositPaid::dispatch($order, $order->deposit_amount); + \Log::info('Yoco Webhook: Order updated for successful deposit payment', [ 'order_uuid' => $orderUuid, 'order_id' => $order->id, @@ -658,10 +669,13 @@ class OrderController extends Controller } elseif ($orderType === 'custom_balance' && $order->balance_status !== 'paid') { $order->update([ 'balance_status' => 'paid', - 'status' => 'in production', + 'status' => 'printing', 'yoco_checkout_response' => json_encode($payload), ]); + // Emit BalancePaid event + BalancePaid::dispatch($order, $order->balance_amount); + \Log::info('Yoco Webhook: Order updated for successful balance payment', [ 'order_uuid' => $orderUuid, 'order_id' => $order->id, @@ -691,17 +705,11 @@ class OrderController extends Controller return response()->json(['status' => 'success']); } - /** - * Show the track order search form - */ public function trackForm() { return view('track-order'); } - /** - * Search for an order by order number and email - */ public function trackSearch(Request $request) { $key = 'track-order:' . $request->ip(); @@ -732,9 +740,6 @@ class OrderController extends Controller return view('track-order-result', ['order' => $order]); } - /** - * Generate basic HTML template for invoice email - */ private function getBasicInvoiceHtml(Order $order): string { return <<header('X-Trello-Webhook'); - if (! $this->verifyWebhookSignature($request, $signature)) { - Log::warning('Invalid Trello webhook signature'); - - return response()->json(['error' => 'Invalid signature'], 401); - } - - try { - $payload = $request->json()->all(); - - // Log webhook for debugging - Log::info('Trello webhook received', ['action' => $payload['action']['type'] ?? 'unknown']); - - // Handle based on action type - match ($payload['action']['type'] ?? null) { - 'updateCard' => $this->handleCardUpdate($payload), - 'updateCheckItem' => $this->handleChecklistUpdate($payload), - default => Log::info('Unhandled Trello action', ['type' => $payload['action']['type'] ?? 'unknown']), - }; - - return response()->json(['success' => true]); - } catch (\Exception $e) { - Log::error('Error processing Trello webhook', ['error' => $e->getMessage()]); - - return response()->json(['error' => 'Processing failed'], 500); - } +{ + // 1️⃣ Trello webhook validation ping (no payload, no signature) + if ($request->getContent() === '' || ! $request->hasHeader('X-Trello-Webhook')) { + Log::info('Trello webhook validation ping received'); + return response()->json(['ok' => true], 200); } + // 2️⃣ Verify webhook signature (real events only) + $signature = $request->header('X-Trello-Webhook'); + if (! $this->verifyWebhookSignature($request, $signature)) { + Log::warning('Invalid Trello webhook signature'); + return response()->json(['error' => 'Invalid signature'], 401); + } + + try { + $payload = $request->json()->all(); + + Log::info('Trello webhook received', [ + 'action' => $payload['action']['type'] ?? 'unknown', + ]); + + match ($payload['action']['type'] ?? null) { + 'updateCard' => $this->handleCardUpdate($payload), + 'updateCheckItem' => $this->handleChecklistUpdate($payload), + default => Log::info('Unhandled Trello action', [ + 'type' => $payload['action']['type'] ?? 'unknown', + ]), + }; + + return response()->json(['success' => true], 200); + } catch (\Throwable $e) { + Log::error('Error processing Trello webhook', [ + 'error' => $e->getMessage(), + ]); + + // ⚠️ Still return 200 so Trello does not disable the webhook + return response()->json(['error' => 'Processing failed'], 200); + } +} + + /** * Handle card movement between lists */ diff --git a/app/Models/CustomOrder.php b/app/Models/CustomOrder.php index 74b8138..ee2fc90 100644 --- a/app/Models/CustomOrder.php +++ b/app/Models/CustomOrder.php @@ -32,10 +32,26 @@ class CustomOrder extends Model 'approved_at', 'rejected_at', 'completed_at', + 'proof_approved', + 'proof_approved_at', + 'packing_width', + 'packing_length', + 'packing_weight', + 'packing_completed_at', + 'packed_by', + 'courier_waybill_id', + 'courier_tracking_number', + 'courier_status', + 'delivered_at', + 'delivery_failure_reason', + 'trello_card_id', + 'qr_token', + 'qr_generated_at', ]; protected $casts = [ 'library_discount_applied' => 'boolean', + 'proof_approved' => 'boolean', 'design_fee' => 'decimal:2', 'material_cost' => 'decimal:2', 'total_cost' => 'decimal:2', @@ -45,6 +61,9 @@ class CustomOrder extends Model 'approved_at' => 'datetime', 'rejected_at' => 'datetime', 'completed_at' => 'datetime', + 'proof_approved_at' => 'datetime', + 'packing_completed_at' => 'datetime', + 'delivered_at' => 'datetime', ]; /** @@ -100,4 +119,17 @@ class CustomOrder extends Model { return 'uuid'; } + + public function packedBy() + { + return $this->belongsTo(User::class, 'packed_by'); + } + + /** + * Check if this is a custom order + */ + public function isCustomOrder(): bool + { + return true; + } } diff --git a/app/Models/Order.php b/app/Models/Order.php index cbc69f9..ad60453 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -31,6 +31,19 @@ class Order extends Model 'yoco_redirect_url', 'yoco_checkout_response', 'yoco_payment_id', + 'packing_width', + 'packing_length', + 'packing_weight', + 'packing_completed_at', + 'packed_by', + 'courier_waybill_id', + 'courier_tracking_number', + 'courier_status', + 'delivered_at', + 'delivery_failure_reason', + 'trello_card_id', + 'qr_token', + 'qr_generated_at', ]; public function user() @@ -42,4 +55,16 @@ class Order extends Model { return $this->hasMany(OrderItem::class, 'order_id', 'uuid'); } -} + + public function packedBy() + { + return $this->belongsTo(User::class, 'packed_by'); + } + + /** + * Check if this is a custom order + */ + public function isCustomOrder(): bool + { + return false; // Standard orders are not custom + } diff --git a/database/migrations/2026_01_02_000002_add_proof_approval_to_custom_orders.php b/database/migrations/2026_01_02_000002_add_proof_approval_to_custom_orders.php new file mode 100644 index 0000000..d62d51d --- /dev/null +++ b/database/migrations/2026_01_02_000002_add_proof_approval_to_custom_orders.php @@ -0,0 +1,26 @@ +boolean('proof_approved')->default(false); + $table->timestamp('proof_approved_at')->nullable(); + } + }); + } + + public function down(): void + { + Schema::table('custom_orders', function (Blueprint $table) { + $table->dropColumn(['proof_approved', 'proof_approved_at']); + }); + } +}; diff --git a/routes/web.php b/routes/web.php index a742ecb..adc1f62 100644 --- a/routes/web.php +++ b/routes/web.php @@ -61,6 +61,9 @@ Route::middleware('auth')->group(function () { 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');