783cc88c6d
- Add integration fields to Order model: packing_*, courier_*, trello_card_id, qr_token
- Add integration fields to CustomOrder model: packing_*, proof_approved_*, courier_*, trello_card_id, qr_token
- Update Order model fillable array and add relationships (packedBy)
- Update CustomOrder model fillable array, casts, and add relationships (packedBy)
- Add isCustomOrder() method to both models for type checking
- Update OrderController to emit OrderCreated and DepositPaid events on successful payment
- For standard orders: full payment -> prep status, emit events
- For custom orders: deposit -> design status, balance -> printing status, emit respective events
- Add approveProof() method to CustomOrderController (POST /custom-orders/{id}/approve-proof)
- Add requestChanges() method to CustomOrderController (POST /custom-orders/{id}/request-changes)
- Add markBalancePaid() method to CustomOrderController (POST /custom-orders/{id}/pay-balance)
- All new methods emit appropriate events (ProofApproved, ProofRevisionRequested, BalancePaid)
- Add database migration for proof_approved and proof_approved_at fields on custom_orders
- Add routes for new custom order endpoints with UUID binding
- Import all required event classes in both controllers
567 lines
20 KiB
PHP
567 lines
20 KiB
PHP
<?php
|
|
|
|
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;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
class CustomOrderController extends Controller
|
|
{
|
|
/**
|
|
* Show custom orders index/list
|
|
*/
|
|
public function index(): View
|
|
{
|
|
$customOrders = CustomOrder::where('user_id', auth()->id())
|
|
->orderBy('created_at', 'desc')
|
|
->get();
|
|
|
|
return view('custom-orders.index', [
|
|
'customOrders' => $customOrders,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Show custom order creation form
|
|
*/
|
|
public function create(): View
|
|
{
|
|
$designFee = AppSetting::get('design_fee', 500);
|
|
|
|
return view('custom-orders.create', [
|
|
'printStocks' => PrintStock::all(),
|
|
'designFee' => $designFee,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Store custom order and calculate quote
|
|
*/
|
|
public function store(Request $request)
|
|
{
|
|
error_log('=== CUSTOM ORDER STORE METHOD CALLED (error_log) ===');
|
|
error_log('Request method: ' . $request->method());
|
|
error_log('Request path: ' . $request->path());
|
|
error_log('Request all data: ' . json_encode($request->all()));
|
|
$userId = auth()->id();
|
|
error_log('User ID: ' . ($userId ?? 'NOT AUTHENTICATED'));
|
|
|
|
Log::warning('=== CUSTOM ORDER STORE METHOD CALLED ===');
|
|
Log::warning('Request all data', $request->all());
|
|
Log::warning('User ID', ['user_id' => auth()->id()]);
|
|
|
|
$validated = $request->validate([
|
|
'type' => 'required|in:wallpaper,mural,fabric',
|
|
'length' => 'nullable|numeric|min:0.1',
|
|
'width' => 'nullable|numeric|min:0.1',
|
|
'height' => 'nullable|numeric|min:0.1',
|
|
'print_stock_id' => 'required|exists:print_stocks,id',
|
|
'quantity' => 'required|integer|min:1',
|
|
'customer_brief' => 'required|string|min:50|max:5000',
|
|
'special_instructions' => 'nullable|string|max:1000',
|
|
'library_discount' => 'boolean',
|
|
'reference_images.*' => 'nullable|file|mimes:jpeg,png,jpg,gif,webp|max:5120',
|
|
]);
|
|
|
|
Log::warning('Validation passed', $validated);
|
|
|
|
// Get design fee from settings
|
|
$designFee = AppSetting::get('design_fee', 500);
|
|
$libraryDiscountApplied = $request->boolean('library_discount', false);
|
|
|
|
// Deduct library discount if selected
|
|
$finalDesignFee = $libraryDiscountApplied ? $designFee * 0.8 : $designFee;
|
|
|
|
// Get print stock for material cost calculation
|
|
$printStock = PrintStock::find($validated['print_stock_id']);
|
|
|
|
// Calculate material cost based on type and dimensions
|
|
$materialCost = $this->calculateMaterialCost(
|
|
$validated['type'],
|
|
$validated['length'] ?? 0,
|
|
$validated['width'] ?? 0,
|
|
$validated['height'] ?? 0,
|
|
$validated['quantity'],
|
|
$printStock
|
|
);
|
|
|
|
// Total cost = material cost + design fee
|
|
$totalCost = $materialCost + $finalDesignFee;
|
|
|
|
// Deposit is 20% of total, balance is 80%
|
|
$depositAmount = $totalCost * 0.2;
|
|
$balanceAmount = $totalCost * 0.8;
|
|
|
|
// Create custom order
|
|
$customOrder = CustomOrder::create([
|
|
'user_id' => auth()->id(),
|
|
'type' => $validated['type'],
|
|
'design_fee' => $designFee,
|
|
'library_discount_applied' => $libraryDiscountApplied,
|
|
'material_cost' => $materialCost,
|
|
'total_cost' => $totalCost,
|
|
'deposit_amount' => $depositAmount,
|
|
'balance_amount' => $balanceAmount,
|
|
'customer_brief' => $validated['customer_brief'],
|
|
]);
|
|
|
|
// Create specifications
|
|
CustomOrderSpecification::create([
|
|
'custom_order_id' => $customOrder->id,
|
|
'length' => $validated['length'] ?? null,
|
|
'width' => $validated['width'] ?? null,
|
|
'height' => $validated['height'] ?? null,
|
|
'print_stock_id' => $validated['print_stock_id'],
|
|
'quantity' => $validated['quantity'],
|
|
'special_instructions' => $validated['special_instructions'] ?? null,
|
|
]);
|
|
|
|
// Handle file uploads
|
|
if ($request->hasFile('reference_images')) {
|
|
foreach ($request->file('reference_images') as $file) {
|
|
$path = $file->store('custom-orders/references', 'local');
|
|
|
|
CustomOrderFile::create([
|
|
'custom_order_id' => $customOrder->id,
|
|
'file_type' => 'reference_image',
|
|
'file_path' => $path,
|
|
'original_filename' => $file->getClientOriginalName(),
|
|
'file_size' => $file->getSize(),
|
|
'mime_type' => $file->getMimeType(),
|
|
'uploaded_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// 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.');
|
|
}
|
|
|
|
/**
|
|
* Show custom order detail with quote
|
|
*/
|
|
public function show(CustomOrder $customOrder): View
|
|
{
|
|
// Check authorization
|
|
if ($customOrder->user_id !== auth()->id()) {
|
|
abort(403);
|
|
}
|
|
|
|
return view('custom-orders.show', [
|
|
'customOrder' => $customOrder,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Process deposit payment
|
|
*/
|
|
public function depositPayment(Request $request)
|
|
{
|
|
Log::info('Deposit payment initiated', [
|
|
'request_data' => $request->all(),
|
|
'user_id' => auth()->id(),
|
|
]);
|
|
$validated = $request->validate([
|
|
'custom_order_id' => 'required|exists:custom_orders,id',
|
|
]);
|
|
Log::info('Deposit payment validation passed', $validated);
|
|
|
|
$customOrder = CustomOrder::findOrFail($validated['custom_order_id']);
|
|
|
|
// Check authorization
|
|
if ($customOrder->user_id !== auth()->id()) {
|
|
Log::warning('Unauthorized deposit payment attempt', [
|
|
'custom_order_id' => $customOrder->id,
|
|
'user_id' => auth()->id(),
|
|
]);
|
|
abort(403);
|
|
}
|
|
|
|
// Check if already paid
|
|
if ($customOrder->deposit_status === 'paid') {
|
|
Log::info('Deposit already paid for custom order', [
|
|
'custom_order_id' => $customOrder->id,
|
|
]);
|
|
return redirect()->route('custom-orders.show', $customOrder)
|
|
->with('info', 'Deposit already paid for this order.');
|
|
}
|
|
|
|
// Initiate Yoco payment for deposit
|
|
Log::info('Initiating Yoco payment for custom order deposit', [
|
|
'custom_order_id' => $customOrder->id,
|
|
'deposit_amount' => $customOrder->deposit_amount,
|
|
]);
|
|
$yocoResponse = $this->initiateYocoPayment(
|
|
amount: (int)($customOrder->deposit_amount * 100), // Convert to cents
|
|
customOrder: $customOrder,
|
|
orderType: 'custom_deposit',
|
|
description: "Deposit for Order #{$customOrder->order_number}"
|
|
);
|
|
|
|
if (!$yocoResponse) {
|
|
Log::error('Failed to initiate Yoco payment for custom order deposit', [
|
|
'custom_order_id' => $customOrder->id,
|
|
]);
|
|
return redirect()->route('custom-orders.show', $customOrder)
|
|
->with('error', 'Failed to initiate payment. Please try again.');
|
|
}
|
|
Log::info('Yoco payment initiated successfully for custom order deposit', [
|
|
'custom_order_id' => $customOrder->id,
|
|
'checkout_url' => $yocoResponse['checkout_url'],
|
|
]);
|
|
return redirect($yocoResponse['checkout_url']);
|
|
}
|
|
|
|
/**
|
|
* Handle successful deposit payment
|
|
*/
|
|
public function depositSuccess(CustomOrder $customOrder)
|
|
{
|
|
// Check authorization
|
|
if ($customOrder->user_id !== auth()->id()) {
|
|
abort(403);
|
|
}
|
|
|
|
// // Update order status
|
|
// $customOrder->update([
|
|
// 'deposit_status' => 'paid',
|
|
// 'status' => 'submitted',
|
|
// ]);
|
|
|
|
return view('custom-orders.deposit-success', [
|
|
'customOrder' => $customOrder,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Calculate material cost based on dimensions and print stock
|
|
*/
|
|
private function calculateMaterialCost($type, $length, $width, $height, $quantity, $printStock)
|
|
{
|
|
$costPerUnit = $printStock->cost_per_meter ?? $printStock->cost_per_m2 ?? 0;
|
|
|
|
if (!$costPerUnit) {
|
|
return 0;
|
|
}
|
|
|
|
$cost = 0;
|
|
|
|
if ($type === 'wallpaper' && $length > 0 && $width > 0) {
|
|
// Area-based: length x width in meters
|
|
$area = $length * $width;
|
|
$cost = $area * $costPerUnit * $quantity;
|
|
} elseif ($type === 'mural' && $width > 0 && $height > 0) {
|
|
// Area-based: width x height in meters
|
|
$area = $width * $height;
|
|
$cost = $area * $costPerUnit * $quantity;
|
|
} elseif ($type === 'fabric' && $length > 0) {
|
|
// Linear: length in meters
|
|
$cost = $length * $costPerUnit * $quantity;
|
|
}
|
|
|
|
return round($cost, 2);
|
|
}
|
|
|
|
/**
|
|
* Initiate Yoco payment
|
|
*/
|
|
private function initiateYocoPayment($amount, $customOrder, $orderType, $description)
|
|
{
|
|
|
|
Log::info('Initiating Yoco payment', [
|
|
'order_id' => $customOrder->uuid,
|
|
'order_type' => $orderType,
|
|
'amount' => $amount,
|
|
'description' => $description,
|
|
]);
|
|
|
|
if (!$customOrder) {
|
|
Log::error('Custom order not found for Yoco payment', [
|
|
'order_id' => $customOrder->uuid ?? 'unknown',
|
|
]);
|
|
return null;
|
|
}
|
|
Log::info('Custom order found for Yoco payment', [
|
|
'order_id' => $customOrder->uuid,
|
|
'custom_order_data' => $customOrder->toArray(),
|
|
]);
|
|
// Get configuration
|
|
$secretKey = config('services.yoco.secret_key');
|
|
$mode = config('services.yoco.mode');
|
|
|
|
Log::info('Yoco configuration', [
|
|
'mode' => $mode,
|
|
'secret_key_set' => !empty($secretKey) && $secretKey !== 'sk_test_your_key_here',
|
|
]);
|
|
|
|
// Check if API key is configured
|
|
if (empty($secretKey) || $secretKey === 'sk_test_your_key_here') {
|
|
Log::error('Yoco secret key not configured');
|
|
return null;
|
|
}
|
|
|
|
$baseUrl = $mode === 'live'
|
|
? 'https://payments.yoco.com/api/checkouts'
|
|
: 'https://payments.yoco.com/api/checkouts';
|
|
|
|
$checkoutData = [
|
|
'amount' => $amount,
|
|
'currency' => 'ZAR',
|
|
'successUrl' => route('yoco-custom-deposit-success', ['customOrder' => $customOrder->uuid]),
|
|
'cancelUrl' => route('custom-orders.show', ['customOrder' => $customOrder->uuid]),
|
|
'failureUrl' => route('custom-orders.show', ['customOrder' => $customOrder->uuid]),
|
|
'metadata' => [
|
|
'order_uuid' => $customOrder->uuid,
|
|
'order_type' => $orderType,
|
|
'site' => 'additional_design',
|
|
'description' => $description
|
|
]
|
|
];
|
|
|
|
Log::info('Yoco checkout data prepared', [
|
|
'order_id' => $customOrder->uuid,
|
|
'checkout_data' => $checkoutData,
|
|
]);
|
|
|
|
// Make API request to Yoco
|
|
|
|
try {
|
|
$response = Http::withHeaders([
|
|
'Authorization' => 'Bearer ' . $secretKey,
|
|
'Content-Type' => 'application/json',
|
|
])->post($baseUrl, $checkoutData);
|
|
|
|
if ($response->successful()) {
|
|
$checkout = $response->json();
|
|
|
|
$checkoutId = $checkout['id'] ?? null;
|
|
$redirectUrl = $checkout['redirectUrl'] ?? null;
|
|
|
|
Log::info('Yoco checkout created for custom order', [
|
|
'order_uuid' => $customOrder->uuid,
|
|
'checkout_id' => $checkoutId,
|
|
'redirect_url' => $redirectUrl,
|
|
]);
|
|
|
|
if (!$checkoutId || !$redirectUrl) {
|
|
Log::error('Invalid Yoco checkout response: missing id or redirectUrl', [
|
|
'order_uuid' => $customOrder->uuid,
|
|
'response' => $checkout,
|
|
]);
|
|
throw new \Exception('Invalid Yoco checkout response: missing id or redirectUrl');
|
|
}
|
|
|
|
// Persist checkout info to database
|
|
try {
|
|
$customOrder->update([
|
|
'yoco_checkout_id' => $checkoutId,
|
|
'yoco_redirect_url' => $redirectUrl,
|
|
'yoco_checkout_response' => json_encode($checkout),
|
|
]);
|
|
|
|
// Reload the model to verify update
|
|
$customOrder->refresh();
|
|
|
|
if ($customOrder->yoco_checkout_id) {
|
|
Log::info('Yoco checkout info saved successfully for custom order', [
|
|
'order_uuid' => $customOrder->uuid,
|
|
'yoco_checkout_id' => $customOrder->yoco_checkout_id,
|
|
]);
|
|
} else {
|
|
Log::warning('Yoco checkout ID not saved after update for custom order', [
|
|
'order_uuid' => $customOrder->uuid,
|
|
'order_data' => $customOrder->toArray(),
|
|
]);
|
|
}
|
|
} catch (\Exception $dbException) {
|
|
Log::error('Database error while saving Yoco checkout info for custom order', [
|
|
'order_uuid' => $customOrder->uuid,
|
|
'error_message' => $dbException->getMessage(),
|
|
'error_code' => $dbException->getCode(),
|
|
'checkout_id' => $checkoutId,
|
|
'redirect_url' => $redirectUrl,
|
|
]);
|
|
throw $dbException;
|
|
}
|
|
|
|
return [
|
|
'checkout_url' => $redirectUrl,
|
|
'checkout_id' => $checkoutId,
|
|
];
|
|
} else {
|
|
Log::error('Yoco API Error for custom order', [
|
|
'order_uuid' => $customOrder->uuid,
|
|
'status' => $response->status(),
|
|
'body' => $response->body()
|
|
]);
|
|
return null;
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error('Yoco Payment Exception for custom order', [
|
|
'order_uuid' => $customOrder->uuid,
|
|
'message' => $e->getMessage()
|
|
]);
|
|
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);
|
|
}
|
|
}
|
|
}
|