Skip to content

[Products]: Import products from external sources #140

Description

@nielsdrost7

SMART Plan — [Products] As a user_admin, I want to import products from external sources

Specific

Add a Filament ImportAction to the ListProducts page that accepts a CSV file with columns code, product_name, description, price, cost_price, category_name, unit_name. Rows missing required fields (code, product_name, price) are skipped and reported back to the user as validation failures.

Measurable

  • An "Import" button appears on the ListProducts header.
  • Uploading a valid 3-row CSV inserts exactly 3 Product records scoped to the current company.
  • A row missing product_name is skipped; no partial record is created.
  • After import, a summary notification shows success count and skipped count with reasons.
  • category_name and unit_name are resolved to existing ProductCategory/ProductUnit records (or created if absent).

Achievable

  1. Create Modules/Products/Filament/Company/Imports/ProductImporter.php implementing Filament's Importer contract.
  2. Add ImportAction::make()->importer(ProductImporter::class) to Modules/Products/Filament/Company/Resources/Products/Pages/ListProducts.php.
  3. Implement resolveRecord() to upsert on code within the current company scope.
  4. Add validation rules in getValidationRules(): code required, product_name required, price required|numeric.
  5. Add PHPUnit tests in Modules/Products/Tests/Feature/ProductsTest.php.

Relevant

Companies migrating from another system or managing products in spreadsheets need a reliable bulk-import path. Manual row-by-row entry for large catalogues is error-prone and time-consuming.

Time-bound

Sprint 5.


Arrange · Act · Assert

// Modules/Products/Tests/Feature/ProductsTest.php

#[Test]
public function it_imports_three_valid_product_rows_from_csv(): void
{
    /* Arrange */
    $csv = "code,product_name,description,price,cost_price,category_name,unit_name\n"
         . "P001,Widget A,A widget,9.99,5.00,Widgets,Each\n"
         . "P002,Widget B,B widget,14.99,7.00,Widgets,Each\n"
         . "P003,Gadget X,A gadget,29.99,15.00,Gadgets,Box\n";

    $file = UploadedFile::fake()->createWithContent('products.csv', $csv);

    /* Act */
    $component = Livewire::actingAs($this->user)
        ->test(ListProducts::class, ['tenant' => Str::lower($this->company->search_code)])
        ->callAction('import', ['file' => $file]);

    /* Assert */
    $component->assertSuccessful();
    $this->assertDatabaseCount('products', 3);
}

#[Test]
public function it_skips_a_row_missing_product_name(): void
{
    /* Arrange */
    $csv = "code,product_name,description,price,cost_price,category_name,unit_name\n"
         . "P001,,A widget,9.99,5.00,Widgets,Each\n"
         . "P002,Widget B,B widget,14.99,7.00,Widgets,Each\n";

    $file = UploadedFile::fake()->createWithContent('products.csv', $csv);

    /* Act */
    $component = Livewire::actingAs($this->user)
        ->test(ListProducts::class, ['tenant' => Str::lower($this->company->search_code)])
        ->callAction('import', ['file' => $file]);

    /* Assert */
    $component->assertSuccessful();
    $this->assertDatabaseCount('products', 1);
}

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