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
157 lines
4.4 KiB
PHP
157 lines
4.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Invoice;
|
|
use App\Models\Order;
|
|
use Illuminate\Support\Str;
|
|
use Spatie\LaravelPdf\Facades\Pdf;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class InvoiceService
|
|
{
|
|
/**
|
|
* Generate an invoice from an order.
|
|
*/
|
|
public function generateFromOrder(Order $order): Invoice
|
|
{
|
|
// Check if invoice already exists for this order
|
|
if ($order->invoice) {
|
|
return $order->invoice;
|
|
}
|
|
|
|
$invoiceNumber = $this->generateInvoiceNumber();
|
|
|
|
$invoice = Invoice::create([
|
|
'invoice_number' => $invoiceNumber,
|
|
'order_id' => $order->id,
|
|
'company_id' => $order->company_id,
|
|
'subtotal' => $order->subtotal,
|
|
'tax' => $order->tax,
|
|
'total' => $order->total,
|
|
'payment_status' => 'unpaid',
|
|
'amount_paid' => 0,
|
|
'amount_due' => $order->total,
|
|
'invoice_date' => now(),
|
|
'due_date' => $order->due_date ?? now()->addDays(30),
|
|
'notes' => $order->notes,
|
|
]);
|
|
|
|
return $invoice;
|
|
}
|
|
|
|
/**
|
|
* Generate a unique invoice number.
|
|
*/
|
|
private function generateInvoiceNumber(): string
|
|
{
|
|
do {
|
|
$number = 'INV-' . strtoupper(Str::random(12));
|
|
} while (Invoice::where('invoice_number', $number)->exists());
|
|
|
|
return $number;
|
|
}
|
|
|
|
/**
|
|
* Record a payment against an invoice.
|
|
*/
|
|
public function recordPayment(Invoice $invoice, float $amount, string $paymentMethod = null, string $notes = null): bool
|
|
{
|
|
if ($amount <= 0) {
|
|
return false;
|
|
}
|
|
|
|
// Update invoice payment status
|
|
$success = $invoice->recordPayment($amount);
|
|
|
|
// TODO: In future, create Payment record to track payment history
|
|
// Payment::create([
|
|
// 'invoice_id' => $invoice->id,
|
|
// 'amount' => $amount,
|
|
// 'payment_method' => $paymentMethod,
|
|
// 'notes' => $notes,
|
|
// ]);
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Update payment status based on current amounts.
|
|
*/
|
|
public function updatePaymentStatus(Invoice $invoice): void
|
|
{
|
|
$invoice->amount_due = bcsub((string) $invoice->total, (string) $invoice->amount_paid, 2);
|
|
|
|
if (bccomp((string) $invoice->amount_due, '0', 2) <= 0) {
|
|
$invoice->payment_status = 'paid';
|
|
$invoice->amount_due = 0;
|
|
} elseif (bccomp((string) $invoice->amount_paid, '0', 2) > 0) {
|
|
$invoice->payment_status = 'partially_paid';
|
|
} else {
|
|
$invoice->payment_status = 'unpaid';
|
|
}
|
|
|
|
// Check if overdue
|
|
if ($invoice->payment_status !== 'paid' && $invoice->due_date->isPast()) {
|
|
$invoice->payment_status = 'overdue';
|
|
}
|
|
|
|
$invoice->save();
|
|
}
|
|
|
|
/**
|
|
* Generate PDF document for invoice.
|
|
*/
|
|
public function generatePdf(Invoice $invoice): string
|
|
{
|
|
// Load all necessary relationships
|
|
$invoice->load([
|
|
'order.items.product.brand',
|
|
'order.company',
|
|
'order.user',
|
|
'company',
|
|
]);
|
|
|
|
// Generate PDF using Blade view with Spatie Laravel PDF
|
|
$pdf = Pdf::view('pdfs.invoice', [
|
|
'invoice' => $invoice,
|
|
'order' => $invoice->order,
|
|
'company' => $invoice->company,
|
|
'items' => $invoice->order->items,
|
|
])
|
|
->format('letter'); // Use letter format for US standard
|
|
|
|
// Generate filename
|
|
$filename = "invoices/{$invoice->invoice_number}.pdf";
|
|
|
|
// Ensure directory exists
|
|
$directory = Storage::disk('local')->path('invoices');
|
|
if (!file_exists($directory)) {
|
|
mkdir($directory, 0755, true);
|
|
}
|
|
|
|
// Save PDF to storage
|
|
$pdf->save(Storage::disk('local')->path($filename));
|
|
|
|
// Update invoice with PDF path
|
|
$invoice->update(['pdf_path' => $filename]);
|
|
|
|
return $filename;
|
|
}
|
|
|
|
/**
|
|
* Regenerate PDF for an existing invoice.
|
|
*/
|
|
public function regeneratePdf(Invoice $invoice): string
|
|
{
|
|
// Delete old PDF if exists
|
|
if ($invoice->pdf_path && Storage::disk('local')->exists($invoice->pdf_path)) {
|
|
Storage::disk('local')->delete($invoice->pdf_path);
|
|
}
|
|
|
|
return $this->generatePdf($invoice);
|
|
}
|
|
}
|