Summary of completed work: - Complete buyer portal (browse, cart, checkout, orders, invoices) - Complete seller portal (orders, manifests, fleet, picking) - Business onboarding wizards (buyer 4-step, seller 5-step) - Email verification and registration flows - Notification system for buyers and sellers - Payment term surcharges and pickup/delivery workflows - Filament admin resources (Business, Brand, Product, Order, Invoice, User) - 51 migrations executed successfully Renamed Company -> Business throughout codebase for consistency. Unified authentication flows with password reset. Added audit trail and telescope for debugging. Next: Week 4 Data Migration (Days 22-28) - Product migration (883 products from cannabrands_crm) - Company migration (81 buyer companies) - User migration (preserve password hashes) - Order history migration
233 lines
7.2 KiB
PHP
233 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Buyer;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Invoice;
|
|
use App\Models\OrderItem;
|
|
use App\Services\OrderModificationService;
|
|
use App\Services\InvoiceService;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class InvoiceController extends Controller
|
|
{
|
|
/**
|
|
* Display a listing of the user's invoices.
|
|
*/
|
|
public function index()
|
|
{
|
|
$user = auth()->user();
|
|
$userBusinessIds = $user->businesses->pluck('id')->toArray();
|
|
|
|
$invoices = Invoice::with(['order', 'business'])
|
|
->whereHas('order', function ($query) use ($user, $userBusinessIds) {
|
|
$query->where('user_id', $user->id)
|
|
->orWhereIn('business_id', $userBusinessIds);
|
|
})
|
|
->latest()
|
|
->get();
|
|
|
|
$stats = [
|
|
'total' => $invoices->count(),
|
|
'unpaid' => $invoices->where('payment_status', 'unpaid')->count(),
|
|
'partially_paid' => $invoices->where('payment_status', 'partially_paid')->count(),
|
|
'paid' => $invoices->where('payment_status', 'paid')->count(),
|
|
'overdue' => $invoices->filter(fn($inv) => $inv->isOverdue())->count(),
|
|
];
|
|
|
|
return view('buyer.invoices.index', compact('invoices', 'stats'));
|
|
}
|
|
|
|
/**
|
|
* Display the specified invoice.
|
|
*/
|
|
public function show(Invoice $invoice)
|
|
{
|
|
// Authorization check
|
|
if (!$this->canAccessInvoice($invoice)) {
|
|
abort(403, 'Unauthorized to view this invoice.');
|
|
}
|
|
|
|
$invoice->load(['order.items', 'business']);
|
|
|
|
// Prepare invoice items data for Alpine.js
|
|
$invoiceItems = $invoice->order->items->map(function($item) {
|
|
return [
|
|
'id' => $item->id,
|
|
'quantity' => $item->picked_qty,
|
|
'originalQuantity' => $item->picked_qty,
|
|
'unit_price' => $item->unit_price,
|
|
'deleted' => false,
|
|
];
|
|
})->values();
|
|
|
|
return view('buyer.invoices.show', compact('invoice', 'invoiceItems'));
|
|
}
|
|
|
|
/**
|
|
* Approve the invoice without modifications.
|
|
*/
|
|
public function approve(Invoice $invoice)
|
|
{
|
|
if (!$this->canAccessInvoice($invoice)) {
|
|
abort(403, 'Unauthorized to approve this invoice.');
|
|
}
|
|
|
|
if (!$invoice->canBeEditedByBuyer()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'This invoice cannot be approved at this time.'
|
|
], 400);
|
|
}
|
|
|
|
$invoice->buyerApprove(auth()->user());
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Invoice approved successfully.'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Reject the invoice.
|
|
*/
|
|
public function reject(Request $request, Invoice $invoice)
|
|
{
|
|
$request->validate([
|
|
'reason' => 'required|string|max:1000',
|
|
]);
|
|
|
|
if (!$this->canAccessInvoice($invoice)) {
|
|
abort(403, 'Unauthorized to reject this invoice.');
|
|
}
|
|
|
|
if (!$invoice->canBeEditedByBuyer()) {
|
|
return back()->with('error', 'This invoice cannot be rejected at this time.');
|
|
}
|
|
|
|
$invoice->buyerReject(auth()->user(), $request->reason);
|
|
|
|
return redirect()->route('buyer.invoices.index')
|
|
->with('success', 'Invoice rejected successfully.');
|
|
}
|
|
|
|
/**
|
|
* Modify the invoice (record buyer's changes).
|
|
*/
|
|
public function modify(Request $request, Invoice $invoice, OrderModificationService $modificationService)
|
|
{
|
|
$request->validate([
|
|
'items' => 'required|array',
|
|
'items.*.id' => 'required|exists:order_items,id',
|
|
'items.*.quantity' => 'required|integer|min:0',
|
|
'items.*.deleted' => 'required|boolean',
|
|
]);
|
|
|
|
if (!$this->canAccessInvoice($invoice)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Unauthorized to modify this invoice.'
|
|
], 403);
|
|
}
|
|
|
|
if (!$invoice->canBeEditedByBuyer()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'This invoice cannot be modified at this time.'
|
|
], 400);
|
|
}
|
|
|
|
// Record all changes
|
|
$hasChanges = false;
|
|
foreach ($request->items as $itemData) {
|
|
$item = OrderItem::find($itemData['id']);
|
|
|
|
// Skip if item doesn't belong to this order
|
|
if ($item->order_id !== $invoice->order_id) {
|
|
continue;
|
|
}
|
|
|
|
// Check for deletion
|
|
if ($itemData['deleted'] && !$item->deleted_at) {
|
|
$modificationService->recordItemDeletion($invoice, $item, auth()->user());
|
|
$hasChanges = true;
|
|
continue;
|
|
}
|
|
|
|
// Check for quantity change
|
|
if ($itemData['quantity'] != $item->picked_qty) {
|
|
// Validate: can only reduce, not increase
|
|
if ($itemData['quantity'] > $item->picked_qty) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'You can only reduce quantities, not increase them.'
|
|
], 400);
|
|
}
|
|
|
|
$modificationService->recordItemChange(
|
|
$invoice,
|
|
$item,
|
|
['quantity' => $itemData['quantity']],
|
|
auth()->user()
|
|
);
|
|
$hasChanges = true;
|
|
}
|
|
}
|
|
|
|
if (!$hasChanges) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No changes detected.'
|
|
], 400);
|
|
}
|
|
|
|
// Update invoice status to buyer_modified
|
|
$invoice->buyerModify();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Changes saved successfully. The seller will review your modifications.'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Download invoice PDF.
|
|
*/
|
|
public function downloadPdf(Invoice $invoice, InvoiceService $invoiceService): Response
|
|
{
|
|
if (!$this->canAccessInvoice($invoice)) {
|
|
abort(403, 'Unauthorized to download this invoice.');
|
|
}
|
|
|
|
// Regenerate PDF if it doesn't exist
|
|
if (!$invoice->pdf_path || !Storage::disk('local')->exists($invoice->pdf_path)) {
|
|
$invoiceService->regeneratePdf($invoice);
|
|
$invoice->refresh();
|
|
}
|
|
|
|
$pdf = Storage::disk('local')->get($invoice->pdf_path);
|
|
|
|
return response($pdf, 200, [
|
|
'Content-Type' => 'application/pdf',
|
|
'Content-Disposition' => 'inline; filename="' . $invoice->invoice_number . '.pdf"',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Check if current user can access the invoice.
|
|
*/
|
|
protected function canAccessInvoice(Invoice $invoice): bool
|
|
{
|
|
$user = auth()->user();
|
|
$userBusinessIds = $user->businesses->pluck('id')->toArray();
|
|
$order = $invoice->order;
|
|
|
|
return $order && (
|
|
$order->user_id === $user->id ||
|
|
in_array($order->business_id, $userBusinessIds)
|
|
);
|
|
}
|
|
}
|