Skip to content

[Quotes]: Give the option to select a quote expiration date rather than having to resort to the Settings page 'auto' date expiration setting #65

Open

Description

@nielsdrost7

SMART Plan — Per-quote custom expiration date

Replacing the previous plan body which contained incorrect AAA tests (raw HTTP routes instead of Filament).

Specific

Add an optional expires_at date field to the quote creation and edit forms. When set, this date overrides the global quote expiry setting from Settings. The field must appear in the quote list table and the quote detail view.

Measurable

  • quotes table has a nullable expires_at (date) column.
  • Creating a quote with expires_at stores the value in the database.
  • Creating a quote without expires_at stores NULL.
  • The column is visible in QuotesTable.
  • Verified by three automated tests (see AAA below).

Achievable

  1. Add migration: $table->date('expires_at')->nullable()->after('quote_date');
  2. Add DatePicker::make('expires_at')->nullable() to QuoteForm
  3. Add TextColumn::make('expires_at')->date() to QuotesTable
  4. Update Quote model to ensure expires_at is fillable and any expiry-related logic prefers expires_at over the global setting when set.

Relevant

Clients negotiate different validity windows per quote (15 days, 30 days, custom). A single global setting cannot accommodate this.

Time-bound

Sprint 5.


Arrange · Act · Assert

// Modules/Quotes/Tests/Feature/QuotesTest.php

#[Test]
public function it_creates_a_quote_with_a_custom_expiry_date(): void
{
    /* Arrange */
    $relation  = Relation::factory()->for($this->company)->customer()->create();
    $expiresAt = now()->addDays(15)->format('Y-m-d');

    /* Act */
    $component = Livewire::actingAs($this->user)
        ->test(CreateQuote::class, ['tenant' => Str::lower($this->company->search_code)])
        ->fillForm([
            'prospect_id'  => $relation->id,
            'expires_at'   => $expiresAt,
        ])
        ->call('create');

    /* Assert */
    $component->assertHasNoFormErrors();

    $this->assertDatabaseHas('quotes', [
        'expires_at' => $expiresAt,
    ]);
}

#[Test]
public function it_creates_a_quote_without_expiry_date(): void
{
    /* Arrange */
    $relation = Relation::factory()->for($this->company)->customer()->create();

    /* Act */
    $component = Livewire::actingAs($this->user)
        ->test(CreateQuote::class, ['tenant' => Str::lower($this->company->search_code)])
        ->fillForm([
            'prospect_id' => $relation->id,
            // expires_at intentionally omitted
        ])
        ->call('create');

    /* Assert */
    $component->assertHasNoFormErrors();

    $this->assertDatabaseHas('quotes', [
        'prospect_id' => $relation->id,
        'expires_at'  => null,
    ]);
}

#[Test]
public function it_shows_expiry_date_column_in_the_quotes_list(): void
{
    /* Arrange */
    Quote::factory()->for($this->company)->create([
        'expires_at' => now()->addDays(30),
    ]);

    /* Act & Assert */
    Livewire::actingAs($this->user)
        ->test(ListQuotes::class, ['tenant' => Str::lower($this->company->search_code)])
        ->assertTableColumnExists('expires_at');
}

Activity

  1. added this to the v2.0.0-alpha milestone on Dec 28, 2025
  2. nielsdrost7 commented on Jul 24, 2026

    @nielsdrost7
    CollaboratorAuthor

    Test Suggestions

    Added to support future implementation of this request. The issue body contains a complete SMART Plan with AAA tests.

    The issue already covers creating quotes with and without expiry dates, and rendering the column in the list. Here are additional scenarios:

    • PHPUnit: it_honors_the_custom_expires_at_over_the_global_setting()
    #[Test]
    public function it_honors_the_custom_expires_at_over_the_global_setting(): void
    {
        /* Arrange */
        $this->company->settings()->set('default_quote_expiry_days', 30);
    
        $relation  = Relation::factory()->for($this->company)->customer()->create();
        $expiresAt = now()->addDays(15)->format('Y-m-d');
    
        /* Act */
        $component = Livewire::actingAs($this->user)
            ->test(CreateQuote::class, ['tenant' => Str::lower($this->company->search_code)])
            ->fillForm([
                'prospect_id' => $relation->id,
                'expires_at'  => $expiresAt,
            ])
            ->call('create');
    
        /* Assert */
        $quote = Quote::where('prospect_id', $relation->id)->first();
        $this->assertNotEquals($quote->expires_at, now()->addDays(30)); // Does NOT use global
        $this->assertEquals($quote->expires_at, $expiresAt);
    }
    • PHPUnit: it_uses_global_setting_when_no_custom_expires_at_is_provided()
    #[Test]
    public function it_uses_global_setting_when_no_custom_expires_at_is_provided(): void
    {
        /* Arrange */
        $this->company->settings()->set('default_quote_expiry_days', 30);
    
        $relation = Relation::factory()->for($this->company)->customer()->create();
    
        /* Act */
        $component = Livewire::actingAs($this->user)
            ->test(CreateQuote::class, ['tenant' => Str::lower($this->company->search_code)])
            ->fillForm([
                'prospect_id' => $relation->id,
                // expires_at intentionally omitted
            ])
            ->call('create');
    
        /* Assert */
        $quote = Quote::where('prospect_id', $relation->id)->first();
        $this->assertEquals($quote->expires_at, now()->addDays(30));
    }
    • Edge case: Can a user set expires_at to a date in the past? Should this be blocked with a validation error, or allowed (for importing old quotes)?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions