Rebuilding my invoicing application in Laravel, Part 3 - Invoice Settings

For the next part of this rebuild, I'll focus on allowing a user to update invoice settings. This is the information that will be used on the invoice page shown to clients.

Goal

For the next part of this rebuild, I'll focus on allowing a user to update invoice settings. This is the information that will be used on the invoice page shown to clients. Also, I'll be moving a bit quicker in this and future posts.

Tests and Development

Again, very basic tests to outline some details of the feature. From here on out I'll skip the validation tests just for the posts (I'll still write them in the project of course).

php artisan make:test Settings/InvoiceSettingsTest

tests/Settings/InvoiceSettingsTest.php

class InvoiceSettingsTest extends TestCase
{
    use RefreshDatabase, ValidationHelperTrait;

    /**
     * Base URI
     *
     * @var string
     */
    protected $base = 'settings/invoice';

    /**
     * @test
     */
    public function guests_can_not_view_invoice_settings_page()
    {
        $this->call('GET', $this->base)->assertRedirect('login');
    }

    /**
     * @test
     */
    public function guests_can_not_update_invoice_settings()
    {
        $this->call('PUT', $this->base)->assertRedirect('login');
    }

    /**
     * @test
     */
    public function invoice_settings_page_loads_with_data()
    {
        $this->actingAsNewUser()
            ->call('GET', $this->base)
            ->assertViewHas('settings');
    }

    /**
     * @test
     */
    public function user_can_update_invoice_settings()
    {
        $user = $this->createUser();

        $data = [
            'company' => 'Test Co.',
            'email' => 'email@email.com',
            'address_1' => '1234 Street',
            'address_2' => 'Suite A',
            'city' => 'Lawrence',
            'state' => 'KS',
            'zip' => '62009',
            'phone' => '123-123-1234',
            'note' => 'Due in 30 days',
        ];

        $this->actingAs($user)
            ->call('PUT', $this->base, $data);

        $this->assertDatabaseHas('invoice_settings', $data);
    }
}

Notice a few difference in these tests versus the previous post's. First, I've added a $base property to make it easer to change the uri or reuse the code. Also, the setting information is going to relate to the user. Although this application is meant to be used by only one user, I still like this approach. Alternatively, this information could be in the config or stored as a key value pair in a "settings" table.

The authentication tests are simple and I addressed them in the previous post. So, I'll just show the build needed to get them to pass.

routes/web.php

Route::get('settings/invoice', 'Settings\InvoiceSettingsController@get');
Route::put('settings/invoice', 'Settings\InvoiceSettingsController@update');

app/Http/Controllers/Settings/InvoiceSettingsController.php

class InvoiceSettingsController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function get()
    {

    }

    public function update()
    {

    }
}

That's it. The page load test should be easy to pass as well.

touch resources/views/settings/invoice.blade.php
public function get()
{
    return view('settings.invoice')
        ->with('settings', $this->getAuthenticatedUser()->invoiceSettings);
}

Now let's get to the main test.

phpunit --filter user_can_update_invoice_settings

ErrorException: Trying to get property of non-object

/invoicing/tests/Feature/Settings/InvoiceSettingsTest.php:77

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

Well of course $user->invoiceSettings is not an object. We will need to update the User model with the relation.

app/User.php

/**
 * Get invoice settings
 * 
 * @return \Illuminate\Database\Eloquent\Relations\HasOne
 */
public function invoiceSettings()
{
    return $this->hasOne(InvoiceSetting::class);
}
phpunit --filter user_can_update_invoice_settings

Error: Class 'Invoicing\InvoiceSetting' not found

Let's create the model with migration and make sure to import the new model in app/User.php.

php artisan make:model Models/InvoiceSetting --migration
phpunit --filter user_can_update_invoice_settings

Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 no such column: invoice_settings.user_id

Seems we need to setup some columns in the migration file.

invoicing/database/migrations/{DATE}_create_invoice_settings_table.php

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('invoice_settings', function (Blueprint $table) {
        $table->increments('id');
        $table->unsignedInteger('user_id');
        $table->string('company');
        $table->string('email');
        $table->string('address_1');
        $table->string('address_2');
        $table->string('city');
        $table->string('state');
        $table->string('zip');
        $table->string('phone');
        $table->text('note');
        $table->timestamps();

        $table->foreign('user_id')->references('id')->on('users')->onUpdate('cascade')->onDelete('cascade');
    });
}

This is a step in the right direction. I'll go ahead and add the update code to the controller and run the test.

app/Http/Controllers/Settings/InvoiceSettingsController.php

public function update(Request $request)
{
    $this->getAuthenticatedUser()->invoiceSettings()->updateOrCreate(array_merge(
        $request->except('logo'),
        [
            'logo' => $this->extractFilenameFromPath($request->file('logo')->store('logos'))
        ]
    ));
}

private function extractFilenameFromPath($path)
{
    $segments = explode('/', $path);

    return end($segments);
}
phpunit --filter user_can_update_invoice_settings

1) Tests\Feature\Settings\InvoiceSettingsTest::user_can_update_invoice_settings
ErrorException: Trying to get property of non-object

/invoicing/tests/Feature/Settings/InvoiceSettingsTest.php:78

It doesn't seem to be saving. Let's look at the log.

Illuminate\\Database\\Eloquent\\MassAssignmentException(code: 0)

Looks like I need to set the $fillable array in the model. That's a simple fix.

app/Models/InvoiceSetting.php

class InvoiceSetting extends Model
{
    protected $fillable = [
        'company',
        'email',
        'address_1',
        'address_2',
        'city',
        'state',
        'zip',
        'phone',
        'note'
    ];
}

All good. Other than the the validation tests, this feature's functionality is ready to go.

2019 Phil Mareu - Coder, Traveler & Disc Thrower