SMART Plan — [Invoices] Guard: prevent deletion of a parent invoice that has a linked credit note
Specific
An invoice cannot be deleted when it has one or more credit notes linked via creditinvoice_parent_id. The DeleteAction on both the InvoicesTable row and the EditInvoice header must be hidden when $invoice->creditNotes()->exists() is true. The service layer must also reject deletion so a direct call cannot bypass the UI check.
Root cause
No guard exists. Deleting a parent invoice leaves its credit note(s) with a dangling creditinvoice_parent_id, breaking the credit note's history and potentially causing errors on the credit note detail page.
Measurable
- The Delete action is hidden on the invoice row when the invoice has one or more credit notes.
- Calling
InvoiceService::deleteInvoice(Invoice) throws InvoiceHasCreditNotesException when a credit note exists.
- Deleting an invoice with no credit notes succeeds normally.
- PHPUnit covers all three scenarios.
Achievable
- Confirm or add a
creditNotes() HasMany relationship on Modules/Invoices/Models/Invoice.php: return $this->hasMany(Invoice::class, 'creditinvoice_parent_id');
- Create
Modules/Invoices/Exceptions/InvoiceHasCreditNotesException.php.
- Add guard in
InvoiceService::deleteInvoice(): throw the exception when $invoice->creditNotes()->withoutGlobalScopes()->exists().
- Add
->hidden(fn (Invoice $record) => $record->creditNotes()->withoutGlobalScopes()->exists()) to the DeleteAction in InvoicesTable and EditInvoice.
- Add PHPUnit tests.
Relevant
Credit notes are financial documents that reverse part or all of an invoice. Deleting the parent while credit notes exist corrupts the accounting trail.
Time-bound
Sprint 5. Related: #171 (create credit notes), #43 (duplicate-number guard for credit notes).
Arrange · Act · Assert
// Modules/Invoices/Tests/Feature/InvoicesTest.php
#[Test]
public function it_throws_when_deleting_an_invoice_that_has_a_credit_note(): void
{
/* Arrange */
$parent = Invoice::factory()->for($this->company)->create([
'invoice_number' => 'INV-2025-001',
'invoice_sign' => '1',
]);
Invoice::factory()->for($this->company)->create([
'invoice_number' => 'INV-2025-001',
'invoice_sign' => '-1',
'creditinvoice_parent_id' => $parent->id,
]);
/* Act & Assert */
$this->expectException(\Modules\Invoices\Exceptions\InvoiceHasCreditNotesException::class);
app(\Modules\Invoices\Services\InvoiceService::class)->deleteInvoice($parent);
}
#[Test]
public function it_allows_deletion_of_an_invoice_without_credit_notes(): void
{
/* Arrange */
$invoice = Invoice::factory()->for($this->company)->create();
/* Act */
app(\Modules\Invoices\Services\InvoiceService::class)->deleteInvoice($invoice);
/* Assert */
$this->assertSoftDeleted('invoices', ['id' => $invoice->id]);
}
#[Test]
public function it_hides_delete_action_on_the_invoice_list_when_credit_note_exists(): void
{
/* Arrange */
$parent = Invoice::factory()->for($this->company)->create(['invoice_sign' => '1']);
Invoice::factory()->for($this->company)->create([
'invoice_sign' => '-1',
'creditinvoice_parent_id' => $parent->id,
]);
/* Act */
$component = Livewire::actingAs($this->user)
->test(ListInvoices::class, ['tenant' => Str::lower($this->company->search_code)]);
/* Assert */
$component->assertSuccessful();
$component->assertTableActionHidden('delete', $parent);
}
SMART Plan — [Invoices] Guard: prevent deletion of a parent invoice that has a linked credit note
Specific
An invoice cannot be deleted when it has one or more credit notes linked via
creditinvoice_parent_id. TheDeleteActionon both theInvoicesTablerow and theEditInvoiceheader must be hidden when$invoice->creditNotes()->exists()is true. The service layer must also reject deletion so a direct call cannot bypass the UI check.Root cause
No guard exists. Deleting a parent invoice leaves its credit note(s) with a dangling
creditinvoice_parent_id, breaking the credit note's history and potentially causing errors on the credit note detail page.Measurable
InvoiceService::deleteInvoice(Invoice)throwsInvoiceHasCreditNotesExceptionwhen a credit note exists.Achievable
creditNotes()HasManyrelationship onModules/Invoices/Models/Invoice.php:return $this->hasMany(Invoice::class, 'creditinvoice_parent_id');Modules/Invoices/Exceptions/InvoiceHasCreditNotesException.php.InvoiceService::deleteInvoice(): throw the exception when$invoice->creditNotes()->withoutGlobalScopes()->exists().->hidden(fn (Invoice $record) => $record->creditNotes()->withoutGlobalScopes()->exists())to theDeleteActioninInvoicesTableandEditInvoice.Relevant
Credit notes are financial documents that reverse part or all of an invoice. Deleting the parent while credit notes exist corrupts the accounting trail.
Time-bound
Sprint 5. Related: #171 (create credit notes), #43 (duplicate-number guard for credit notes).
Arrange · Act · Assert