Files
hub/app/Services/InvoiceService.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

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