mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
624 lines
26 KiB
PHP
624 lines
26 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Importing\Api;
|
|
|
|
use App\Models\Accessory;
|
|
use App\Models\Actionlog;
|
|
use App\Models\Import;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\WithFaker;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Testing\TestResponse;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\Concerns\TestsPermissionsRequirement;
|
|
use Tests\Support\Importing\AccessoriesImportFileBuilder as ImportFileBuilder;
|
|
use Tests\Support\Importing\CleansUpImportFiles;
|
|
|
|
class ImportAccessoriesTest extends ImportDataTestCase implements TestsPermissionsRequirement
|
|
{
|
|
use CleansUpImportFiles;
|
|
use WithFaker;
|
|
|
|
protected function importFileResponse(array $parameters = []): TestResponse
|
|
{
|
|
if (! array_key_exists('import-type', $parameters)) {
|
|
$parameters['import-type'] = 'accessory';
|
|
}
|
|
|
|
return parent::importFileResponse($parameters);
|
|
}
|
|
|
|
#[Test]
|
|
public function test_requires_permission()
|
|
{
|
|
$this->actingAsForApi(User::factory()->create());
|
|
|
|
$this->importFileResponse(['import' => 44])->assertForbidden();
|
|
}
|
|
|
|
#[Test]
|
|
public function user_with_import_accessory_permission_can_import_accessories(): void
|
|
{
|
|
$this->actingAsForApi(User::factory()->canImport()->create());
|
|
|
|
$import = Import::factory()->accessory()->create();
|
|
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
}
|
|
|
|
#[Test]
|
|
public function import_accessory(): void
|
|
{
|
|
$importFileBuilder = ImportFileBuilder::new();
|
|
$row = $importFileBuilder->firstRow();
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])
|
|
->assertOk()
|
|
->assertExactJson([
|
|
'payload' => ['tally' => ['created' => 1, 'updated' => 0, 'skipped' => 0, 'errored' => 0]],
|
|
'status' => 'success',
|
|
'messages' => [
|
|
'redirect_url' => route('accessories.index'),
|
|
],
|
|
]);
|
|
|
|
$newAccessory = Accessory::query()
|
|
->with(['location', 'category', 'manufacturer', 'defaultSupplier', 'company'])
|
|
->where('name', $row['itemName'])
|
|
->sole();
|
|
|
|
$activityLog = Actionlog::query()
|
|
->where('item_type', Accessory::class)
|
|
->where('item_id', $newAccessory->id)
|
|
->sole();
|
|
|
|
$this->assertEquals('create', $activityLog->action_type);
|
|
$this->assertEquals('importer', $activityLog->action_source);
|
|
$this->assertEquals($newAccessory->company->id, $activityLog->company_id);
|
|
|
|
$this->assertEquals($row['itemName'], $newAccessory->name);
|
|
$this->assertEquals($row['quantity'], $newAccessory->qty);
|
|
// supplier + order_number + purchase_date + purchase_cost all
|
|
// live on the Orders / OrderItems polymorphic pair now — the
|
|
// importer's recordOrderForImportedRow helper writes them there.
|
|
$orderItem = $newAccessory->orderItems()->firstOrFail();
|
|
$this->assertEquals($row['orderNumber'], $orderItem->order->order_number);
|
|
$this->assertEquals($row['purchaseDate'], $orderItem->order->purchase_date->toDateString());
|
|
$this->assertEquals((float) $row['purchaseCost'], (float) $orderItem->price);
|
|
$this->assertEquals($row['supplierName'], $orderItem->order->supplier->name);
|
|
$this->assertEquals($row['notes'], $newAccessory->notes);
|
|
$this->assertEquals($row['category'], $newAccessory->category->name);
|
|
$this->assertEquals('accessory', $newAccessory->category->category_type);
|
|
$this->assertEquals($row['manufacturerName'], $newAccessory->manufacturer->name);
|
|
$this->assertEquals($row['location'], $newAccessory->location->name);
|
|
$this->assertEquals($row['companyName'], $newAccessory->company->name);
|
|
$this->assertEquals($row['modelNumber'], $newAccessory->model_number);
|
|
$this->assertFalse($newAccessory->requestable);
|
|
$this->assertNull($newAccessory->min_amt);
|
|
$this->assertNull($newAccessory->user_id);
|
|
}
|
|
|
|
#[Test]
|
|
public function when_import_file_contains_unknown_columns(): void
|
|
{
|
|
$row = ImportFileBuilder::new()->definition();
|
|
$row['unknownColumn'] = $this->faker->word;
|
|
|
|
$importFileBuilder = new ImportFileBuilder([$row]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
}
|
|
|
|
#[Test]
|
|
public function will_format_date(): void
|
|
{
|
|
$importFileBuilder = ImportFileBuilder::new(['purchaseDate' => '2022/10/10']);
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
// purchase_date landed on the OrderItem's Order rather than the
|
|
// parent Accessory column — assert that the importer's date
|
|
// parser normalized the slashed CSV input to a Y-m-d value.
|
|
$accessory = Accessory::query()
|
|
->where('name', $importFileBuilder->firstRow()['itemName'])
|
|
->sole();
|
|
|
|
$order = $accessory->orderItems()->latest('id')->firstOrFail()->order;
|
|
$this->assertEquals('2022-10-10', $order->purchase_date->toDateString());
|
|
}
|
|
|
|
#[Test]
|
|
public function will_not_create_new_category_when_category_exists(): void
|
|
{
|
|
$importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => Str::random()]);
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$newAccessories = Accessory::query()
|
|
->whereIn('name', $importFileBuilder->pluck('itemName'))
|
|
->get();
|
|
|
|
$this->assertCount(1, $newAccessories->pluck('category_id')->unique()->all());
|
|
}
|
|
|
|
#[Test]
|
|
public function will_not_create_new_accessory_when_accessory_with_name_exists(): void
|
|
{
|
|
$accessory = Accessory::factory()->create(['name' => Str::random()]);
|
|
$importFileBuilder = ImportFileBuilder::times(2)->replace(['itemName' => $accessory->name]);
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$probablyNewAccessories = Accessory::query()
|
|
->where('name', $importFileBuilder->pluck('itemName'))
|
|
->get(['name']);
|
|
|
|
$this->assertCount(1, $probablyNewAccessories);
|
|
$this->assertEquals($accessory->name, $probablyNewAccessories->first()->name);
|
|
}
|
|
|
|
#[Test]
|
|
public function will_not_create_new_company_when_company_already_exists(): void
|
|
{
|
|
$importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]);
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$newAccessories = Accessory::query()
|
|
->where('name', $importFileBuilder->pluck('itemName'))
|
|
->get(['company_id']);
|
|
|
|
$this->assertCount(1, $newAccessories->pluck('company_id')->unique()->all());
|
|
}
|
|
|
|
#[Test]
|
|
public function will_not_create_new_location_when_location_already_exists(): void
|
|
{
|
|
$importFileBuilder = ImportFileBuilder::times(4)->replace(['location' => Str::random()]);
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$newAccessories = Accessory::query()
|
|
->where('name', $importFileBuilder->pluck('itemName'))
|
|
->get(['location_id']);
|
|
|
|
$this->assertCount(1, $newAccessories->pluck('location_id')->unique()->all());
|
|
}
|
|
|
|
#[Test]
|
|
public function will_not_create_new_manufacturer_when_manufacturer_already_exists(): void
|
|
{
|
|
$importFileBuilder = ImportFileBuilder::times(4)->replace(['manufacturerName' => $this->faker->company]);
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$newAccessories = Accessory::query()
|
|
->where('name', $importFileBuilder->pluck('itemName'))
|
|
->get(['manufacturer_id']);
|
|
|
|
$this->assertCount(1, $newAccessories->pluck('manufacturer_id')->unique()->all());
|
|
}
|
|
|
|
#[Test]
|
|
public function will_not_create_new_supplier_when_supplier_already_exists(): void
|
|
{
|
|
$importFileBuilder = ImportFileBuilder::times(4)->replace(['supplierName' => $this->faker->company]);
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
// Supplier reuse across imported rows is observable on the
|
|
// OrderItem's Order.supplier_id (parent's default_supplier_id
|
|
// gets seeded from the first-row's supplier for these but the
|
|
// per-row dedupe rule lives on the Order path).
|
|
$newAccessories = Accessory::query()
|
|
->whereIn('name', $importFileBuilder->pluck('itemName'))
|
|
->get();
|
|
|
|
$supplierIds = $newAccessories->map(
|
|
fn ($accessory) => $accessory->orderItems()->latest('id')->first()?->order?->supplier_id,
|
|
)->filter()->unique()->all();
|
|
|
|
$this->assertCount(1, $supplierIds);
|
|
}
|
|
|
|
#[Test]
|
|
public function when_columns_are_missing_in_import_file(): void
|
|
{
|
|
$importFileBuilder = ImportFileBuilder::new()->forget(['minimumAmount', 'purchaseCost', 'purchaseDate']);
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$newAccessory = Accessory::query()
|
|
->where('name', $importFileBuilder->firstRow()['itemName'])
|
|
->sole();
|
|
|
|
$this->assertNull($newAccessory->min_amt);
|
|
// purchase_date / purchase_cost columns are gone from the parent;
|
|
// when the CSV omits them, the observer-written OrderItem has
|
|
// null price and its Order has null purchase_date.
|
|
$orderItem = $newAccessory->orderItems()->latest('id')->firstOrFail();
|
|
$this->assertNull($orderItem->order->purchase_date);
|
|
$this->assertNull($orderItem->price);
|
|
}
|
|
|
|
#[Test]
|
|
public function when_required_columns_are_missing_in_import_file(): void
|
|
{
|
|
$importFileBuilder = ImportFileBuilder::new()->forget(['itemName', 'category']);
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])
|
|
->assertInternalServerError()
|
|
->assertExactJson([
|
|
'status' => 'import-errors',
|
|
'payload' => ['tally' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errored' => 1]],
|
|
'messages' => [
|
|
'' => [
|
|
'Accessory' => [
|
|
'category_id' => ['The category id field is required.'],
|
|
'name' => ['The name field is required.'],
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
#[Test]
|
|
public function update_accessory_from_import(): void
|
|
{
|
|
$accessory = Accessory::factory()->create(['name' => Str::random()])->refresh();
|
|
$importFileBuilder = ImportFileBuilder::new(['itemName' => $accessory->name]);
|
|
$row = $importFileBuilder->firstRow();
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
|
|
|
|
$updatedAccessory = Accessory::query()->find($accessory->id);
|
|
$updatedAttributes = [
|
|
'name', 'company_id', 'qty', 'default_purchase_cost', 'default_supplier_id',
|
|
'notes', 'category_id', 'manufacturer_id',
|
|
'location_id', 'model_number', 'updated_at',
|
|
];
|
|
|
|
$this->assertEquals($row['itemName'], $updatedAccessory->name);
|
|
$this->assertEquals($row['companyName'], $updatedAccessory->company->name);
|
|
$this->assertEquals($row['quantity'], $updatedAccessory->qty);
|
|
// Update mode does NOT rewrite historical Orders — the CSV's
|
|
// purchase_cost / supplier map to the parent's default_*
|
|
// template fields; purchase_date has no forward-use equivalent
|
|
// and is silently dropped. When qty differs the value rides on
|
|
// the QuantityAdjust log (see importer_qty_change_creates_...).
|
|
$this->assertEquals((float) $row['purchaseCost'], (float) $updatedAccessory->default_purchase_cost);
|
|
$this->assertEquals($row['supplierName'], $updatedAccessory->defaultSupplier->name);
|
|
$this->assertEquals($row['notes'], $updatedAccessory->notes);
|
|
$this->assertEquals($row['category'], $updatedAccessory->category->name);
|
|
$this->assertEquals('accessory', $updatedAccessory->category->category_type);
|
|
$this->assertEquals($row['manufacturerName'], $updatedAccessory->manufacturer->name);
|
|
$this->assertEquals($row['location'], $updatedAccessory->location->name);
|
|
$this->assertEquals($row['modelNumber'], $updatedAccessory->model_number);
|
|
|
|
$this->assertEquals(
|
|
Arr::except($accessory->attributesToArray(), $updatedAttributes),
|
|
Arr::except($updatedAccessory->attributesToArray(), $updatedAttributes),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function update_mode_logs_accessory_update_in_actionlog(): void
|
|
{
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
|
|
$initialFile = ImportFileBuilder::new();
|
|
$initialRow = $initialFile->firstRow();
|
|
|
|
$initialImport = Import::factory()->accessory()->create([
|
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
|
]);
|
|
|
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
|
|
|
$accessory = Accessory::query()->where('name', $initialRow['itemName'])->sole();
|
|
|
|
// Change `notes` (a plain fillable field). orderNumber is
|
|
// intentionally NOT the trigger here because
|
|
// ItemImporter::applyUpdateWithQtyAdjust strips order_number from
|
|
// the update payload, so a notes-only diff is what proves the
|
|
// update-log path still fires.
|
|
$updatedRow = array_merge($initialRow, [
|
|
'notes' => (string) ($initialRow['notes'] ?? '').' updated',
|
|
]);
|
|
|
|
$updateFile = new ImportFileBuilder([$updatedRow]);
|
|
$updateImport = Import::factory()->accessory()->create([
|
|
'file_path' => $updateFile->saveToImportsDirectory(),
|
|
]);
|
|
|
|
$this->importFileResponse([
|
|
'import' => $updateImport->id,
|
|
'import-update' => true,
|
|
])->assertOk();
|
|
|
|
$accessory->refresh();
|
|
$this->assertEquals($updatedRow['notes'], $accessory->notes);
|
|
|
|
$updateLog = Actionlog::query()
|
|
->where('item_type', Accessory::class)
|
|
->where('item_id', $accessory->id)
|
|
->where('action_type', 'update')
|
|
->latest('id')
|
|
->first();
|
|
|
|
$this->assertNotNull($updateLog, 'Expected an update action log entry after accessory importer update mode.');
|
|
}
|
|
|
|
#[Test]
|
|
public function update_mode_clears_field_when_csv_column_is_present_but_empty(): void
|
|
{
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
|
|
$accessory = Accessory::factory()->create([
|
|
'notes' => 'Some pre-existing notes',
|
|
])->refresh();
|
|
|
|
$this->assertNotEmpty($accessory->notes);
|
|
|
|
// purchase_date moved off the parent post-Orders — the
|
|
// "empty CSV cell clears the DB column" behavior for the
|
|
// parent-owned columns is covered here by notes alone.
|
|
$row = ImportFileBuilder::new()->definition();
|
|
$row['itemName'] = $accessory->name;
|
|
$row['notes'] = '';
|
|
|
|
$importFileBuilder = new ImportFileBuilder([$row]);
|
|
$import = Import::factory()->accessory()->create([
|
|
'file_path' => $importFileBuilder->saveToImportsDirectory(),
|
|
]);
|
|
|
|
$this->importFileResponse([
|
|
'import' => $import->id,
|
|
'import-update' => true,
|
|
])->assertOk();
|
|
|
|
$accessory->refresh();
|
|
$this->assertNull($accessory->notes);
|
|
}
|
|
|
|
#[Test]
|
|
public function update_mode_preserves_fields_when_csv_column_is_absent(): void
|
|
{
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
|
|
$accessory = Accessory::factory()->create([
|
|
'notes' => 'Do not lose this',
|
|
])->refresh();
|
|
|
|
$originalNotes = $accessory->notes;
|
|
|
|
// Import a CSV that only has the identity field (name) plus one
|
|
// updated column. All other parent columns absent from the CSV
|
|
// must be preserved on update. model_number is the "changed
|
|
// field" proxy; notes is the "preserved field" proxy.
|
|
$partialFile = new ImportFileBuilder([[
|
|
'itemName' => $accessory->name,
|
|
'modelNumber' => 'UPDATED-MODEL-NUMBER',
|
|
]]);
|
|
$partialImport = Import::factory()->accessory()->create([
|
|
'file_path' => $partialFile->saveToImportsDirectory(),
|
|
]);
|
|
|
|
$this->importFileResponse([
|
|
'import' => $partialImport->id,
|
|
'import-update' => true,
|
|
])->assertOk();
|
|
|
|
$accessory->refresh();
|
|
$this->assertEquals('UPDATED-MODEL-NUMBER', $accessory->model_number);
|
|
$this->assertEquals($originalNotes, $accessory->notes);
|
|
}
|
|
|
|
#[Test]
|
|
public function when_import_file_contains_empty_values(): void
|
|
{
|
|
$accessory = Accessory::factory()->create(['name' => Str::random()]);
|
|
$accessory->refresh();
|
|
|
|
$importFileBuilder = ImportFileBuilder::new([
|
|
'companyName' => ' ',
|
|
'purchaseDate' => ' ',
|
|
'purchaseCost' => '',
|
|
'location' => '',
|
|
'orderNumber' => '',
|
|
'category' => '',
|
|
'quantity' => '',
|
|
'manufacturerName' => '',
|
|
'supplierName' => '',
|
|
'notes' => '',
|
|
'requestAble' => '',
|
|
'minimumAmount' => '',
|
|
'modelNumber' => '',
|
|
]);
|
|
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])
|
|
->assertInternalServerError()
|
|
->assertExactJson([
|
|
'status' => 'import-errors',
|
|
'payload' => ['tally' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errored' => 1]],
|
|
'messages' => [
|
|
$importFileBuilder->firstRow()['itemName'] => [
|
|
'Accessory' => [
|
|
'category_id' => ['The category id field is required.'],
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$importFileBuilder->replace(['itemName' => $accessory->name]);
|
|
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
|
|
|
|
$updatedAccessory = clone $accessory;
|
|
$updatedAccessory->refresh();
|
|
|
|
$this->assertEquals($accessory->toArray(), $updatedAccessory->toArray());
|
|
}
|
|
|
|
#[Test]
|
|
public function custom_column_mapping(): void
|
|
{
|
|
$faker = ImportFileBuilder::new()->definition();
|
|
$row = [
|
|
'itemName' => $faker['modelNumber'],
|
|
'purchaseDate' => $faker['notes'],
|
|
'purchaseCost' => $faker['location'],
|
|
'location' => $faker['purchaseCost'],
|
|
'companyName' => $faker['orderNumber'],
|
|
'orderNumber' => $faker['companyName'],
|
|
'category' => $faker['manufacturerName'],
|
|
'manufacturerName' => $faker['category'],
|
|
'notes' => $faker['purchaseDate'],
|
|
'minimumAmount' => $faker['supplierName'],
|
|
'modelNumber' => $faker['itemName'],
|
|
'quantity' => $faker['quantity'],
|
|
];
|
|
|
|
$importFileBuilder = new ImportFileBuilder([$row]);
|
|
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse([
|
|
'import' => $import->id,
|
|
'column-mappings' => [
|
|
'Item Name' => 'model_number',
|
|
'Purchase Date' => 'notes',
|
|
'Purchase Cost' => 'location',
|
|
'Location' => 'purchase_cost',
|
|
'Company' => 'order_number',
|
|
'Order Number' => 'company',
|
|
'Category' => 'manufacturer',
|
|
'Manufacturer' => 'category',
|
|
'Supplier' => 'min_amt',
|
|
'Notes' => 'purchase_date',
|
|
'Min QTY' => 'supplier',
|
|
'Model Number' => 'item_name',
|
|
'Quantity' => 'quantity',
|
|
],
|
|
])->assertOk();
|
|
|
|
$newAccessory = Accessory::query()
|
|
->with(['location', 'category', 'manufacturer'])
|
|
->where('name', $row['modelNumber'])
|
|
->sole();
|
|
|
|
// purchase_date, purchase_cost, and supplier moved off the
|
|
// parent to the Orders / OrderItems polymorphic pair —
|
|
// recordOrderForImportedRow persists them there. Assertions
|
|
// walk through orderItems.order for those three fields.
|
|
$this->assertEquals($row['modelNumber'], $newAccessory->name);
|
|
$this->assertEquals($row['itemName'], $newAccessory->model_number);
|
|
$this->assertEquals($row['quantity'], $newAccessory->qty);
|
|
|
|
$orderItem = $newAccessory->orderItems()->firstOrFail();
|
|
$this->assertEquals($row['notes'], $orderItem->order->purchase_date->toDateString());
|
|
$this->assertEquals($row['location'], (float) $orderItem->price);
|
|
// See the import_accessory test above for why order_number now
|
|
// lives on Orders / OrderItems rather than the parent column.
|
|
// Note this custom-mapping test intentionally maps companyName
|
|
// to the orderNumber CSV column, verifying that whatever value
|
|
// that column carries lands on the OrderItem's Order regardless
|
|
// of what the source column was called.
|
|
$this->assertEquals($row['companyName'], $orderItem->order->order_number);
|
|
$this->assertEquals($row['purchaseDate'], $newAccessory->notes);
|
|
$this->assertEquals($row['manufacturerName'], $newAccessory->category->name);
|
|
$this->assertEquals($row['category'], $newAccessory->manufacturer->name);
|
|
$this->assertEquals($row['purchaseCost'], $newAccessory->location->name);
|
|
}
|
|
|
|
#[Test]
|
|
public function accessory_import_checks_out_to_user_when_username_matches(): void
|
|
{
|
|
$actor = User::factory()->superuser()->create();
|
|
$target = User::factory()->create(['username' => 'accessorytarget']);
|
|
|
|
// Hand-crafted CSV: needs the checkout columns
|
|
// (checkout_class + username) alongside the standard accessory
|
|
// columns, which the AccessoriesImportFileBuilder doesn't include.
|
|
$csv = "Item Name,Category,Quantity,Company,Checkout Type,Username\n"
|
|
.'CSV-Checked-Accessory,Cables,5,CSVCo,user,'.$target->username."\n";
|
|
$filename = 'accessory-checkout-'.uniqid().'.csv';
|
|
file_put_contents(config('app.private_uploads').'/imports/'.$filename, $csv);
|
|
|
|
try {
|
|
$import = Import::factory()->accessory()->create(['file_path' => $filename]);
|
|
|
|
$this->actingAsForApi($actor);
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$accessory = Accessory::query()->where('name', 'CSV-Checked-Accessory')->sole();
|
|
$this->assertEquals(5, $accessory->qty);
|
|
|
|
$this->assertDatabaseHas('accessories_checkout', [
|
|
'accessory_id' => $accessory->id,
|
|
'assigned_to' => $target->id,
|
|
'assigned_type' => User::class,
|
|
]);
|
|
} finally {
|
|
@unlink(config('app.private_uploads').'/imports/'.$filename);
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function accessory_import_without_checkout_columns_does_not_create_checkout(): void
|
|
{
|
|
$actor = User::factory()->superuser()->create();
|
|
|
|
$csv = "Item Name,Category,Quantity,Company\n"
|
|
."CSV-Uncheckedout-Accessory,Cables,3,CSVCo\n";
|
|
$filename = 'accessory-nocheckout-'.uniqid().'.csv';
|
|
file_put_contents(config('app.private_uploads').'/imports/'.$filename, $csv);
|
|
|
|
try {
|
|
$import = Import::factory()->accessory()->create(['file_path' => $filename]);
|
|
|
|
$this->actingAsForApi($actor);
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$accessory = Accessory::query()->where('name', 'CSV-Uncheckedout-Accessory')->sole();
|
|
$this->assertDatabaseMissing('accessories_checkout', [
|
|
'accessory_id' => $accessory->id,
|
|
]);
|
|
} finally {
|
|
@unlink(config('app.private_uploads').'/imports/'.$filename);
|
|
}
|
|
}
|
|
}
|