Files
hub/app/Http/Controllers/Buyer/InvoiceController.php
Jon Leopard 7e5ea2bc10 checkpoint: marketplace and operational features complete, ready for Week 4 data migration
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
2025-10-15 11:17:15 -07:00

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)
);
}
}