Allow invoices to target either a client or a client group by making `client_id` nullable and validating that exactly one recipient is set. Load group relations in invoice data so the frontend can display group details in invoice views and type definitions. Make quote acceptance reuse an existing invoice instead of creating a duplicate, and surface backend status update errors in the quote UI.
204 lines
7.5 KiB
PHP
204 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Repositories;
|
|
|
|
use App\Models\Invoice;
|
|
use App\Models\Quote;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
use App\Repositories\ClientActivityTimelineRepositoryInterface;
|
|
|
|
class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInterface
|
|
{
|
|
public function __construct(
|
|
Invoice $model,
|
|
protected InvoiceLineRepositoryInterface $invoiceLineRepository,
|
|
protected readonly ClientActivityTimelineRepositoryInterface $timelineRepository
|
|
) {
|
|
parent::__construct($model);
|
|
}
|
|
|
|
public function createFromQuote(int|string $quoteId): Invoice
|
|
{
|
|
return DB::transaction(function () use ($quoteId) {
|
|
// Resolve Quote directly to avoid circular dependency
|
|
$quote = Quote::with(['client', 'group', 'lines'])->find($quoteId);
|
|
|
|
if (!$quote) {
|
|
throw new \Exception("Quote not found");
|
|
}
|
|
|
|
$existingInvoice = Invoice::where('source_quote_id', $quote->id)->first();
|
|
if ($existingInvoice) {
|
|
return $existingInvoice;
|
|
}
|
|
|
|
// Create Invoice
|
|
$invoiceData = [
|
|
'client_id' => $quote->client_id,
|
|
'group_id' => $quote->group_id,
|
|
'source_quote_id' => $quote->id,
|
|
'status' => 'brouillon', // Start as draft
|
|
'invoice_date' => now(),
|
|
'currency' => $quote->currency,
|
|
'total_ht' => $quote->total_ht,
|
|
'total_tva' => $quote->total_tva,
|
|
'total_ttc' => $quote->total_ttc,
|
|
];
|
|
|
|
$invoice = parent::create($invoiceData);
|
|
|
|
// Copy Lines
|
|
foreach ($quote->lines as $line) {
|
|
$this->invoiceLineRepository->create([
|
|
'invoice_id' => $invoice->id,
|
|
'product_id' => $line->product_id,
|
|
'packaging_id' => $line->packaging_id,
|
|
'packages_qty' => $line->packages_qty,
|
|
'units_qty' => $line->units_qty,
|
|
'description' => $line->description,
|
|
'qty_base' => $line->qty_base,
|
|
'unit_price' => $line->unit_price,
|
|
'unit_price_per_package' => $line->unit_price_per_package,
|
|
'tva_rate_id' => $line->tva_rate_id,
|
|
'discount_pct' => $line->discount_pct,
|
|
'total_ht' => $line->total_ht,
|
|
]);
|
|
}
|
|
|
|
// Record history
|
|
$this->recordHistory($invoice->id, null, 'brouillon', 'Created from Quote ' . $quote->reference);
|
|
|
|
try {
|
|
if ($invoice->client_id !== null) {
|
|
$this->timelineRepository->logActivity([
|
|
'client_id' => $invoice->client_id,
|
|
'actor_type' => 'user',
|
|
'actor_user_id' => auth()->id(),
|
|
'event_type' => 'invoice_created',
|
|
'entity_type' => 'invoice',
|
|
'entity_id' => $invoice->id,
|
|
'title' => 'Nouvelle facture créée',
|
|
'description' => "Une facture a été créée à partir du devis #{$quote->id}.",
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error("Failed to log invoice creation activity: " . $e->getMessage());
|
|
}
|
|
|
|
return $invoice;
|
|
});
|
|
}
|
|
|
|
|
|
|
|
public function all(array $columns = ['*']): \Illuminate\Support\Collection
|
|
{
|
|
return $this->model->with(['client', 'group', 'lines.product'])->get($columns);
|
|
}
|
|
|
|
public function create(array $data): Invoice
|
|
{
|
|
return DB::transaction(function () use ($data) {
|
|
try {
|
|
// Create the invoice
|
|
$invoice = parent::create($data);
|
|
|
|
// Create the invoice lines
|
|
if (isset($data['lines']) && is_array($data['lines'])) {
|
|
foreach ($data['lines'] as $lineData) {
|
|
$lineData['invoice_id'] = $invoice->id;
|
|
$this->invoiceLineRepository->create($lineData);
|
|
}
|
|
}
|
|
|
|
// Record initial status history
|
|
$this->recordHistory($invoice->id, null, $invoice->status, 'Invoice created');
|
|
|
|
try {
|
|
if ($invoice->client_id !== null) {
|
|
$this->timelineRepository->logActivity([
|
|
'client_id' => $invoice->client_id,
|
|
'actor_type' => 'user',
|
|
'actor_user_id' => auth()->id(),
|
|
'event_type' => 'invoice_created',
|
|
'entity_type' => 'invoice',
|
|
'entity_id' => $invoice->id,
|
|
'title' => 'Nouvelle facture créée',
|
|
'description' => "La facture #{$invoice->id} a été créée.",
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error("Failed to log manual invoice creation activity: " . $e->getMessage());
|
|
}
|
|
|
|
return $invoice;
|
|
} catch (\Exception $e) {
|
|
Log::error('Error creating invoice with lines: ' . $e->getMessage(), [
|
|
'exception' => $e,
|
|
'data' => $data,
|
|
]);
|
|
throw $e;
|
|
}
|
|
});
|
|
}
|
|
|
|
public function update(int|string $id, array $attributes): bool
|
|
{
|
|
return DB::transaction(function () use ($id, $attributes) {
|
|
try {
|
|
$invoice = $this->find($id);
|
|
if (!$invoice) {
|
|
return false;
|
|
}
|
|
|
|
$oldStatus = $invoice->status;
|
|
|
|
// Update the invoice
|
|
$updated = parent::update($id, $attributes);
|
|
|
|
if ($updated) {
|
|
$newStatus = $attributes['status'] ?? $oldStatus;
|
|
|
|
// If status changed, record history
|
|
if ($oldStatus !== $newStatus) {
|
|
$this->recordHistory((int) $id, $oldStatus, $newStatus, 'Invoice status updated');
|
|
}
|
|
}
|
|
|
|
return $updated;
|
|
} catch (\Exception $e) {
|
|
Log::error('Error updating invoice: ' . $e->getMessage(), [
|
|
'id' => $id,
|
|
'attributes' => $attributes,
|
|
'exception' => $e,
|
|
]);
|
|
throw $e;
|
|
}
|
|
});
|
|
}
|
|
|
|
public function find(int|string $id, array $columns = ['*']): ?Invoice
|
|
{
|
|
return $this->model->with(['client', 'group', 'lines.product', 'history.user'])->find($id, $columns);
|
|
}
|
|
|
|
private function recordHistory(int $invoiceId, ?string $oldStatus, string $newStatus, ?string $comment = null): void
|
|
{
|
|
\App\Models\DocumentStatusHistory::create([
|
|
'document_type' => 'invoice',
|
|
'document_id' => $invoiceId,
|
|
'old_status' => $oldStatus,
|
|
'new_status' => $newStatus,
|
|
'changed_by' => auth()->id(), // Assuming authenticated user
|
|
'comment' => $comment,
|
|
'changed_at' => now(),
|
|
]);
|
|
}
|
|
}
|