php-commerce/Services/InvoiceService.php

252 lines
7.7 KiB
PHP
Raw Normal View History

2026-01-27 00:24:22 +00:00
<?php
namespace Core\Mod\Commerce\Services;
2026-01-27 00:24:22 +00:00
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Core\Mod\Commerce\Mail\InvoiceGenerated;
use Core\Mod\Commerce\Models\Invoice;
use Core\Mod\Commerce\Models\InvoiceItem;
use Core\Mod\Commerce\Models\Order;
use Core\Mod\Commerce\Models\Payment;
2026-01-27 00:24:22 +00:00
use Core\Mod\Tenant\Models\Workspace;
/**
* Invoice generation and management service.
*/
class InvoiceService
{
public function __construct(
protected TaxService $taxService,
) {}
/**
* Create an invoice from an order.
*/
public function createFromOrder(Order $order, ?Payment $payment = null): Invoice
{
$amountDue = $payment ? 0 : $order->total;
// Resolve workspace ID from polymorphic orderable (Workspace or User)
$workspaceId = $order->workspace_id;
$invoice = Invoice::create([
'workspace_id' => $workspaceId,
'order_id' => $order->id,
'payment_id' => $payment?->id,
'invoice_number' => Invoice::generateInvoiceNumber(),
'status' => $payment ? 'paid' : 'pending',
'subtotal' => $order->subtotal,
'discount_amount' => $order->discount_amount ?? 0,
'tax_amount' => $order->tax_amount ?? 0,
'tax_rate' => $order->tax_rate ?? 0,
'tax_country' => $order->tax_country,
'total' => $order->total,
'amount_paid' => $payment ? $order->total : 0,
'amount_due' => $amountDue,
'currency' => $order->currency,
'billing_name' => $order->billing_name,
'billing_email' => $order->billing_email,
'billing_address' => $order->billing_address,
'issue_date' => now(),
'due_date' => now()->addDays(config('commerce.billing.invoice_due_days', 14)),
'paid_at' => $payment ? now() : null,
]);
// Copy line items from order
foreach ($order->items as $orderItem) {
InvoiceItem::create([
'invoice_id' => $invoice->id,
'order_item_id' => $orderItem->id,
'description' => $orderItem->description,
'quantity' => $orderItem->quantity,
'unit_price' => $orderItem->unit_price,
'line_total' => $orderItem->line_total,
'tax_rate' => $order->tax_rate ?? 0,
'tax_amount' => ($orderItem->line_total - $orderItem->unit_price * $orderItem->quantity),
]);
}
return $invoice;
}
/**
* Create an invoice for a subscription renewal.
*/
public function createForRenewal(
Workspace $workspace,
float $amount,
string $description,
?Payment $payment = null
): Invoice {
$taxResult = $this->taxService->calculate($workspace, $amount);
$total = $amount + $taxResult->taxAmount;
$amountDue = $payment ? 0 : $total;
$invoice = Invoice::create([
'workspace_id' => $workspace->id,
'invoice_number' => Invoice::generateInvoiceNumber(),
'status' => $payment ? 'paid' : 'pending',
'subtotal' => $amount,
'discount_amount' => 0,
'tax_amount' => $taxResult->taxAmount,
'tax_rate' => $taxResult->taxRate,
'tax_country' => $taxResult->jurisdiction,
'total' => $total,
'amount_paid' => $payment ? $total : 0,
'amount_due' => $amountDue,
'currency' => config('commerce.currency', 'GBP'),
'billing_name' => $workspace->billing_name,
'billing_email' => $workspace->billing_email,
'billing_address' => $workspace->getBillingAddress(),
'issue_date' => now(),
'due_date' => now()->addDays(config('commerce.billing.invoice_due_days', 14)),
'paid_at' => $payment ? now() : null,
'payment_id' => $payment?->id,
]);
InvoiceItem::create([
'invoice_id' => $invoice->id,
'description' => $description,
'quantity' => 1,
'unit_price' => $amount,
'line_total' => $total,
'tax_rate' => $taxResult->taxRate,
'tax_amount' => $taxResult->taxAmount,
]);
return $invoice;
}
/**
* Mark invoice as paid.
*/
public function markAsPaid(Invoice $invoice, Payment $payment): void
{
$invoice->markAsPaid($payment);
}
/**
* Mark invoice as void.
*/
public function void(Invoice $invoice): void
{
$invoice->void();
}
/**
* Generate PDF for an invoice.
*/
public function generatePdf(Invoice $invoice): string
{
$invoice->load(['workspace', 'items']);
$pdf = Pdf::loadView('commerce::pdf.invoice', [
'invoice' => $invoice,
'business' => config('commerce.tax.business'),
]);
$filename = $this->getPdfPath($invoice);
Storage::disk(config('commerce.pdf.storage_disk', 'local'))
->put($filename, $pdf->output());
// Update invoice with PDF path
$invoice->update(['pdf_path' => $filename]);
return $filename;
}
/**
* Get or generate PDF for invoice.
*/
public function getPdf(Invoice $invoice): string
{
if ($invoice->pdf_path && Storage::disk(config('commerce.pdf.storage_disk', 'local'))->exists($invoice->pdf_path)) {
return $invoice->pdf_path;
}
return $this->generatePdf($invoice);
}
/**
* Get PDF download response.
*/
public function downloadPdf(Invoice $invoice): \Symfony\Component\HttpFoundation\StreamedResponse
{
$path = $this->getPdf($invoice);
return Storage::disk(config('commerce.pdf.storage_disk', 'local'))
->download($path, "invoice-{$invoice->invoice_number}.pdf");
}
/**
* Get PDF path for an invoice.
*/
protected function getPdfPath(Invoice $invoice): string
{
$basePath = config('commerce.pdf.storage_path', 'invoices');
return "{$basePath}/{$invoice->workspace_id}/{$invoice->invoice_number}.pdf";
}
/**
* Send invoice email.
*/
public function sendEmail(Invoice $invoice): void
{
if (! config('commerce.billing.send_invoice_emails', true)) {
return;
}
// Generate PDF if not exists
$this->getPdf($invoice);
// Determine recipient email
$recipientEmail = $invoice->billing_email
?? $invoice->workspace?->billing_email
?? $invoice->workspace?->owner()?->email;
if (! $recipientEmail) {
return;
}
Mail::to($recipientEmail)->queue(new InvoiceGenerated($invoice));
}
/**
* Get invoices for a workspace.
*/
public function getForWorkspace(Workspace $workspace, int $limit = 25): \Illuminate\Pagination\LengthAwarePaginator
{
return $workspace->invoices()
->with('items')
->latest()
->paginate($limit);
}
/**
* Get unpaid invoices for a workspace.
*/
public function getUnpaidForWorkspace(Workspace $workspace): \Illuminate\Database\Eloquent\Collection
{
return $workspace->invoices()
->pending()
->where('due_date', '>=', now())
->get();
}
/**
* Get overdue invoices for a workspace.
*/
public function getOverdueForWorkspace(Workspace $workspace): \Illuminate\Database\Eloquent\Collection
{
return $workspace->invoices()
->pending()
->where('due_date', '<', now())
->get();
}
}