Fixed FD-52058 - Importer using wrong created_at date
This commit is contained in:
@@ -37,7 +37,8 @@ class AccessoryImporter extends ItemImporter
|
|||||||
$this->log('Updating Accessory');
|
$this->log('Updating Accessory');
|
||||||
$this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
|
$this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
|
||||||
$accessory->update($this->sanitizeItemForUpdating($accessory));
|
$accessory->update($this->sanitizeItemForUpdating($accessory));
|
||||||
$accessory->save();
|
// update() already saves the model, no need to call save() again while Model::unguard() is active
|
||||||
|
$accessory->setImported(true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,35 +176,55 @@ class AssetImporter extends ItemImporter
|
|||||||
|
|
||||||
if ($editingAsset) {
|
if ($editingAsset) {
|
||||||
$asset->update($item);
|
$asset->update($item);
|
||||||
|
$asset->setImported(true);
|
||||||
} else {
|
} else {
|
||||||
$asset->fill($item);
|
$asset->fill($item);
|
||||||
|
$asset->setImported(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're updating, we don't want to overwrite old fields.
|
// If we're updating, we don't want to overwrite old fields.
|
||||||
|
// Apply custom fields to asset attributes if they exist
|
||||||
|
$customFieldsToSave = [];
|
||||||
if (array_key_exists('custom_fields', $this->item)) {
|
if (array_key_exists('custom_fields', $this->item)) {
|
||||||
foreach ($this->item['custom_fields'] as $custom_field => $val) {
|
foreach ($this->item['custom_fields'] as $custom_field => $val) {
|
||||||
$asset->{$custom_field} = $val;
|
$asset->{$custom_field} = $val;
|
||||||
|
$customFieldsToSave[$custom_field] = $val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This sets an attribute on the Loggable trait for the action log
|
// For existing assets that have custom fields, update them.
|
||||||
$asset->setImported(true);
|
// This avoids the issue of calling save() twice with Model::unguard() active.
|
||||||
|
if ($editingAsset && !empty($customFieldsToSave)) {
|
||||||
|
$asset->update($customFieldsToSave);
|
||||||
|
$success = true;
|
||||||
|
} elseif (!$editingAsset) {
|
||||||
|
// For new assets, save with all changes (custom fields included via direct attribute assignment above)
|
||||||
|
$success = $asset->save();
|
||||||
|
} else {
|
||||||
|
// For existing assets without custom fields, update() already saved everything
|
||||||
|
$success = true;
|
||||||
|
}
|
||||||
|
|
||||||
if ($asset->save()) {
|
if ($success) {
|
||||||
|
|
||||||
$this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
|
$this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' created or updated');
|
||||||
|
|
||||||
// If we have a target to checkout to, lets do so.
|
// If we have a target to checkout to, lets do so.
|
||||||
// -- created_by is a property of the abstract class Importer, which this class inherits from and it's set by
|
// -- created_by is a property of the abstract class Importer, which this class inherits from and it's set by
|
||||||
// -- the class that needs to use it (command importer or GUI importer inside the project).
|
// -- the class that needs to use it (command importer or GUI importer inside the project).
|
||||||
if (isset($target) && ($target !== false)) {
|
if (isset($target) && ($target !== false)) {
|
||||||
if (! is_null($asset->assigned_to)) {
|
$asset = $asset->fresh();
|
||||||
if ($asset->assigned_to != $target->id) {
|
$targetType = get_class($target);
|
||||||
|
$alreadyCheckedOutToTarget = ($asset->assigned_to == $target->id) && ($asset->assigned_type === $targetType);
|
||||||
|
|
||||||
|
// Skip duplicate checkout noise when update mode keeps the same assignment target.
|
||||||
|
if (! $alreadyCheckedOutToTarget) {
|
||||||
|
if (! is_null($asset->assigned_to)) {
|
||||||
event(new CheckoutableCheckedIn($asset, $asset->assigned, auth()->user(), 'Checkin from CSV Importer', $checkin_date));
|
event(new CheckoutableCheckedIn($asset, $asset->assigned, auth()->user(), 'Checkin from CSV Importer', $checkin_date));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$asset->fresh()->checkOut($target, $this->created_by, $checkout_date, null, 'Checkout from CSV Importer', $asset->name);
|
$asset->checkOut($target, $this->created_by, $checkout_date, null, 'Checkout from CSV Importer', $asset->name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ class ComponentImporter extends ItemImporter
|
|||||||
}
|
}
|
||||||
$this->log('Updating Component');
|
$this->log('Updating Component');
|
||||||
$component->update($this->sanitizeItemForUpdating($component));
|
$component->update($this->sanitizeItemForUpdating($component));
|
||||||
$component->save();
|
// update() already saves the model, no need to call save() again while Model::unguard() is active
|
||||||
|
$component->setImported(true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ class ConsumableImporter extends ItemImporter
|
|||||||
}
|
}
|
||||||
$this->log('Updating Consumable');
|
$this->log('Updating Consumable');
|
||||||
$consumable->update($this->sanitizeItemForUpdating($consumable));
|
$consumable->update($this->sanitizeItemForUpdating($consumable));
|
||||||
$consumable->save();
|
// update() already saves the model, no need to call save() again while Model::unguard() is active
|
||||||
|
$consumable->setImported(true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,8 +88,12 @@ class LicenseImporter extends ItemImporter
|
|||||||
|
|
||||||
// This sets an attribute on the Loggable trait for the action log
|
// This sets an attribute on the Loggable trait for the action log
|
||||||
$license->setImported(true);
|
$license->setImported(true);
|
||||||
if ($license->save()) {
|
|
||||||
$this->log('License '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
|
// For new licenses we need to save, for existing ones update() already saved
|
||||||
|
$licenseWasSaved = $editingLicense || $license->save();
|
||||||
|
|
||||||
|
if ($licenseWasSaved) {
|
||||||
|
$this->log('License '.$this->item['name'].' with serial number '.$this->item['serial'].' was created or updated');
|
||||||
|
|
||||||
// Lets try to checkout seats if the fields exist and we have seats.
|
// Lets try to checkout seats if the fields exist and we have seats.
|
||||||
if ($license->seats > 0) {
|
if ($license->seats > 0) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Company,Name,Asset Tag,Category,Supplier,Manufacturer,Location,Order Number,Model,Model Notes,Model Number,Asset Notes,Purchase Date,Purchase Cost,Checkout Type,Checked Out To: Username,Checked Out To: First Name,Checked Out To: Last Name,Checked Out To: Email,Checked Out To: Location,Asset EOL Date
|
Company,Name,Asset Tag,Category,Supplier,Manufacturer,Location,Order Number,Model,Model Notes,Model Number,Asset Notes,Purchase Date,Purchase Cost,Checkout Type,Checked Out To: Username,Checked Out To: First Name,Checked Out To: Last Name,Checked Out To: Email,Checked Out To: Location,Asset EOL Date
|
||||||
Abshire and Sons,Backhoe,ICC-2065556,Ornamental Railings,"Kunde, Doyle and Kozey",Berge Inc,"Wilkinson, Waters and Kerluke",3271901481,"Macbook Pro 13""",,1786VM80X07,at nulla suspendisse potenti cras in purus eu magna vulputate luctus cum sociis natoque penatibus et magnis dis,1/23/23,2266.13,,,,,,,10/27/28
|
Abshire and Sons and Sons and Sons and Sons!,Backhoe,ICC-2065556,Ornamental Railings,"Kunde, Doyle and Kozey",Berge Inc,"Wilkinson, Waters and Kerluke",3271901481,"Macbook Pro 13""",,1786VM80X07,at nulla suspendisse potenti cras in purus eu magna vulputate luctus cum sociis natoque penatibus et magnis dis,1/23/23,2266.13,,,,,,,10/27/28
|
||||||
"Quitzon, Oberbrunner and Dibbert",Dragline,WBH-2841795,Structural and Misc Steel (Fabrication),Krajcik LLC,"Botsford, Boyle and Herzog",Lindgren-Marquardt,5504512275,"Macbook Pro 13""",ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae mauris viverra diam vitae quam suspendisse potenti nullam,9351IS25A51,aliquam convallis nunc proin at turpis a pede posuere nonummy integer,11/14/22,1292.94,User,gmccrackem2a,Gage,McCrackem,gmccrackem2a@bing.com,,10/27/28
|
"Quitzon, Oberbrunner and Dibbert",Dragline,WBH-2841795,Structural and Misc Steel (Fabrication),Krajcik LLC,"Botsford, Boyle and Herzog",Lindgren-Marquardt,5504512275,"Macbook Pro 13""",ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae mauris viverra diam vitae quam suspendisse potenti nullam,9351IS25A51,aliquam convallis nunc proin at turpis a pede posuere nonummy integer,11/14/22,1292.94,User,gmccrackem2a,Gage,McCrackem,gmccrackem2a@bing.com,,10/27/28
|
||||||
Boyer and Sons,Excavator,NNH-3656031,Soft Flooring and Base,"Heaney, Altenwerth and Emmerich",Pollich LLC,Pacocha-Kiehn,4861125177,"Macbook Pro 13""",,9929FR08W85,,3/1/23,2300.71,Location,,,,,Pacocha-Kiehn,10/27/28
|
Boyer and Sons,Excavator,NNH-3656031,Soft Flooring and Base,"Heaney, Altenwerth and Emmerich",Pollich LLC,Pacocha-Kiehn,4861125177,"Macbook Pro 13""",,9929FR08W85,,3/1/23,2300.71,Location,,,,,Pacocha-Kiehn,10/27/28
|
||||||
Hayes-Rippin,Trencher,BOL-0305383,Prefabricated Aluminum Metal Canopies,"Botsford, Boyle and Herzog",Walker-Towne,Fritsch-Abernathy,2416994639,"Macbook Pro 13""",neque vestibulum eget vulputate ut ultrices vel augue vestibulum ante ipsum primis in faucibus orci luctus,9139KQ78G81,,10/26/22,1777.56,User,ksennett6,Katerina,Sennett,ksennett6@ibm.com,,10/27/28
|
Hayes-Rippin,Trencher,BOL-0305383,Prefabricated Aluminum Metal Canopies,"Botsford, Boyle and Herzog",Walker-Towne,Fritsch-Abernathy,2416994639,"Macbook Pro 13""",neque vestibulum eget vulputate ut ultrices vel augue vestibulum ante ipsum primis in faucibus orci luctus,9139KQ78G81,,10/26/22,1777.56,User,ksennett6,Katerina,Sennett,ksennett6@ibm.com,,10/27/28
|
||||||
|
|||||||
|
@@ -305,6 +305,49 @@ class ImportAccessoriesTest extends ImportDataTestCase implements TestsPermissio
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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();
|
||||||
|
|
||||||
|
$updatedRow = array_merge($initialRow, [
|
||||||
|
'orderNumber' => (string) $initialRow['orderNumber'].'-UPD',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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['orderNumber'], $accessory->order_number);
|
||||||
|
|
||||||
|
$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]
|
#[Test]
|
||||||
public function when_import_file_contains_empty_values(): void
|
public function when_import_file_contains_empty_values(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Importing\Api;
|
namespace Tests\Feature\Importing\Api;
|
||||||
|
|
||||||
|
use App\Models\Actionlog as ActionLog;
|
||||||
use App\Models\AssetModel;
|
use App\Models\AssetModel;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\Import;
|
use App\Models\Import;
|
||||||
@@ -131,4 +132,46 @@ class ImportAssetModelsTest extends ImportDataTestCase implements TestsPermissio
|
|||||||
$this->assertEquals($row['name'], $updatedAssetmodel->name);
|
$this->assertEquals($row['name'], $updatedAssetmodel->name);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function update_mode_logs_asset_model_update_in_actionlog(): void
|
||||||
|
{
|
||||||
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||||
|
|
||||||
|
$initialFile = ImportFileBuilder::new();
|
||||||
|
$initialRow = $initialFile->firstRow();
|
||||||
|
$initialImport = Import::factory()->assetmodel()->create([
|
||||||
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
||||||
|
|
||||||
|
$assetModel = AssetModel::query()->where('name', $initialRow['name'])->sole();
|
||||||
|
|
||||||
|
$updatedRow = array_merge($initialRow, [
|
||||||
|
'model_number' => Str::random(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updateFile = new ImportFileBuilder([$updatedRow]);
|
||||||
|
$updateImport = Import::factory()->assetmodel()->create([
|
||||||
|
'file_path' => $updateFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse([
|
||||||
|
'import' => $updateImport->id,
|
||||||
|
'import-update' => true,
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$assetModel->refresh();
|
||||||
|
$this->assertEquals($updatedRow['model_number'], $assetModel->model_number);
|
||||||
|
|
||||||
|
$updateLog = ActionLog::query()
|
||||||
|
->where('item_type', AssetModel::class)
|
||||||
|
->where('item_id', $assetModel->id)
|
||||||
|
->where('action_type', 'update')
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($updateLog, 'Expected an update action log entry after asset model importer update mode.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -427,6 +427,58 @@ class ImportAssetsTest extends ImportDataTestCase implements TestsPermissionsReq
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function update_mode_logs_asset_update_in_actionlog(): void
|
||||||
|
{
|
||||||
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||||
|
|
||||||
|
$initialFile = ImportFileBuilder::new();
|
||||||
|
$initialRow = $initialFile->firstRow();
|
||||||
|
|
||||||
|
$initialImport = Import::factory()->asset()->create([
|
||||||
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
||||||
|
|
||||||
|
$asset = Asset::query()->where('asset_tag', $initialRow['tag'])->sole();
|
||||||
|
|
||||||
|
$updatedRow = array_merge($initialRow, [
|
||||||
|
'itemName' => $initialRow['itemName'].' Updated',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updateFile = new ImportFileBuilder([$updatedRow]);
|
||||||
|
$updateImport = Import::factory()->asset()->create([
|
||||||
|
'file_path' => $updateFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse([
|
||||||
|
'import' => $updateImport->id,
|
||||||
|
'import-update' => true,
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$asset->refresh();
|
||||||
|
$this->assertEquals($updatedRow['itemName'], $asset->name);
|
||||||
|
|
||||||
|
$updateLog = ActionLog::query()
|
||||||
|
->where('item_type', Asset::class)
|
||||||
|
->where('item_id', $asset->id)
|
||||||
|
->where('action_type', 'update')
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($updateLog, 'Expected an update action log entry after importer update mode.');
|
||||||
|
$this->assertStringContainsString('name', (string) $updateLog->log_meta);
|
||||||
|
|
||||||
|
$checkoutLogsCount = ActionLog::query()
|
||||||
|
->where('item_type', Asset::class)
|
||||||
|
->where('item_id', $asset->id)
|
||||||
|
->where('action_type', 'checkout')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$this->assertSame(1, $checkoutLogsCount, 'Re-import update should not create a duplicate checkout log for the same assignment.');
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function custom_column_mapping(): void
|
public function custom_column_mapping(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -249,6 +249,52 @@ class ImportComponentsTest extends ImportDataTestCase implements TestsPermission
|
|||||||
$this->assertEquals($component->notes, $updatedComponent->notes);
|
$this->assertEquals($component->notes, $updatedComponent->notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function update_mode_logs_component_update_in_actionlog(): void
|
||||||
|
{
|
||||||
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||||
|
|
||||||
|
$initialFile = ImportFileBuilder::new();
|
||||||
|
$initialRow = $initialFile->firstRow();
|
||||||
|
|
||||||
|
$initialImport = Import::factory()->component()->create([
|
||||||
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
||||||
|
|
||||||
|
$component = Component::query()
|
||||||
|
->where('name', $initialRow['itemName'])
|
||||||
|
->where('serial', $initialRow['serialNumber'])
|
||||||
|
->sole();
|
||||||
|
|
||||||
|
$updatedRow = array_merge($initialRow, [
|
||||||
|
'orderNumber' => (string) $initialRow['orderNumber'].'-UPD',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updateFile = new ImportFileBuilder([$updatedRow]);
|
||||||
|
$updateImport = Import::factory()->component()->create([
|
||||||
|
'file_path' => $updateFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse([
|
||||||
|
'import' => $updateImport->id,
|
||||||
|
'import-update' => true,
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$component->refresh();
|
||||||
|
$this->assertEquals($updatedRow['orderNumber'], $component->order_number);
|
||||||
|
|
||||||
|
$updateLog = ActionLog::query()
|
||||||
|
->where('item_type', Component::class)
|
||||||
|
->where('item_id', $component->id)
|
||||||
|
->where('action_type', 'update')
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($updateLog, 'Expected an update action log entry after component importer update mode.');
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function custom_column_mapping(): void
|
public function custom_column_mapping(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -243,6 +243,49 @@ class ImportConsumablesTest extends ImportDataTestCase implements TestsPermissio
|
|||||||
$this->assertEquals($consumable->item_number, $updatedConsumable->item_number);
|
$this->assertEquals($consumable->item_number, $updatedConsumable->item_number);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function update_mode_logs_consumable_update_in_actionlog(): void
|
||||||
|
{
|
||||||
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||||
|
|
||||||
|
$initialFile = ImportFileBuilder::new();
|
||||||
|
$initialRow = $initialFile->firstRow();
|
||||||
|
|
||||||
|
$initialImport = Import::factory()->consumable()->create([
|
||||||
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
||||||
|
|
||||||
|
$consumable = Consumable::query()->where('name', $initialRow['itemName'])->sole();
|
||||||
|
|
||||||
|
$updatedRow = array_merge($initialRow, [
|
||||||
|
'orderNumber' => (string) $initialRow['orderNumber'].'-UPD',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updateFile = new ImportFileBuilder([$updatedRow]);
|
||||||
|
$updateImport = Import::factory()->consumable()->create([
|
||||||
|
'file_path' => $updateFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse([
|
||||||
|
'import' => $updateImport->id,
|
||||||
|
'import-update' => true,
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$consumable->refresh();
|
||||||
|
$this->assertEquals($updatedRow['orderNumber'], $consumable->order_number);
|
||||||
|
|
||||||
|
$updateLog = ActivityLog::query()
|
||||||
|
->where('item_type', Consumable::class)
|
||||||
|
->where('item_id', $consumable->id)
|
||||||
|
->where('action_type', 'update')
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($updateLog, 'Expected an update action log entry after consumable importer update mode.');
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function custom_column_mapping(): void
|
public function custom_column_mapping(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -277,6 +277,52 @@ class ImportLicenseTest extends ImportDataTestCase implements TestsPermissionsRe
|
|||||||
$this->assertEquals($license->min_amt, $updatedLicense->min_amt);
|
$this->assertEquals($license->min_amt, $updatedLicense->min_amt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function update_mode_logs_license_update_in_actionlog(): void
|
||||||
|
{
|
||||||
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||||
|
|
||||||
|
$initialFile = ImportFileBuilder::new();
|
||||||
|
$initialRow = $initialFile->firstRow();
|
||||||
|
|
||||||
|
$initialImport = Import::factory()->license()->create([
|
||||||
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
||||||
|
|
||||||
|
$license = License::query()
|
||||||
|
->where('name', $initialRow['licenseName'])
|
||||||
|
->where('serial', $initialRow['serialNumber'])
|
||||||
|
->sole();
|
||||||
|
|
||||||
|
$updatedRow = array_merge($initialRow, [
|
||||||
|
'orderNumber' => (string) $initialRow['orderNumber'].'-UPD',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updateFile = new ImportFileBuilder([$updatedRow]);
|
||||||
|
$updateImport = Import::factory()->license()->create([
|
||||||
|
'file_path' => $updateFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse([
|
||||||
|
'import' => $updateImport->id,
|
||||||
|
'import-update' => true,
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$license->refresh();
|
||||||
|
$this->assertEquals($updatedRow['orderNumber'], $license->order_number);
|
||||||
|
|
||||||
|
$updateLog = ActivityLog::query()
|
||||||
|
->where('item_type', License::class)
|
||||||
|
->where('item_id', $license->id)
|
||||||
|
->where('action_type', 'update')
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($updateLog, 'Expected an update action log entry after license importer update mode.');
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function custom_column_mapping(): void
|
public function custom_column_mapping(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Importing\Api;
|
namespace Tests\Feature\Importing\Api;
|
||||||
|
|
||||||
|
use App\Models\Actionlog as ActionLog;
|
||||||
use App\Models\Import;
|
use App\Models\Import;
|
||||||
use App\Models\Location;
|
use App\Models\Location;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@@ -129,4 +130,46 @@ class ImportLocationsTest extends ImportDataTestCase implements TestsPermissions
|
|||||||
Arr::except($updatedLocation->attributesToArray(), array_merge($updatedAttributes, $location->getDates())),
|
Arr::except($updatedLocation->attributesToArray(), array_merge($updatedAttributes, $location->getDates())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function update_mode_logs_location_update_in_actionlog(): void
|
||||||
|
{
|
||||||
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||||
|
|
||||||
|
$initialFile = ImportFileBuilder::new();
|
||||||
|
$initialRow = $initialFile->firstRow();
|
||||||
|
$initialImport = Import::factory()->locations()->create([
|
||||||
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
||||||
|
|
||||||
|
$location = Location::query()->where('name', $initialRow['name'])->sole();
|
||||||
|
|
||||||
|
$updatedRow = array_merge($initialRow, [
|
||||||
|
'notes' => 'Importer update notes',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updateFile = new ImportFileBuilder([$updatedRow]);
|
||||||
|
$updateImport = Import::factory()->locations()->create([
|
||||||
|
'file_path' => $updateFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse([
|
||||||
|
'import' => $updateImport->id,
|
||||||
|
'import-update' => true,
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$location->refresh();
|
||||||
|
$this->assertEquals($updatedRow['notes'], $location->notes);
|
||||||
|
|
||||||
|
$updateLog = ActionLog::query()
|
||||||
|
->where('item_type', Location::class)
|
||||||
|
->where('item_id', $location->id)
|
||||||
|
->where('action_type', 'update')
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($updateLog, 'Expected an update action log entry after location importer update mode.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Importing\Api;
|
namespace Tests\Feature\Importing\Api;
|
||||||
|
|
||||||
|
use App\Models\Actionlog as ActionLog;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\Import;
|
use App\Models\Import;
|
||||||
use App\Models\Location;
|
use App\Models\Location;
|
||||||
@@ -260,6 +261,48 @@ class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequ
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function update_mode_logs_user_update_in_actionlog(): void
|
||||||
|
{
|
||||||
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||||
|
|
||||||
|
$initialFile = ImportFileBuilder::new();
|
||||||
|
$initialRow = $initialFile->firstRow();
|
||||||
|
$initialImport = Import::factory()->users()->create([
|
||||||
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
||||||
|
|
||||||
|
$user = User::query()->where('username', $initialRow['username'])->sole();
|
||||||
|
|
||||||
|
$updatedRow = array_merge($initialRow, [
|
||||||
|
'position' => $initialRow['position'].' Updated',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updateFile = new ImportFileBuilder([$updatedRow]);
|
||||||
|
$updateImport = Import::factory()->users()->create([
|
||||||
|
'file_path' => $updateFile->saveToImportsDirectory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->importFileResponse([
|
||||||
|
'import' => $updateImport->id,
|
||||||
|
'import-update' => true,
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$user->refresh();
|
||||||
|
$this->assertEquals($updatedRow['position'], $user->jobtitle);
|
||||||
|
|
||||||
|
$updateLog = ActionLog::query()
|
||||||
|
->where('item_type', User::class)
|
||||||
|
->where('item_id', $user->id)
|
||||||
|
->where('action_type', 'update')
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($updateLog, 'Expected an update action log entry after user importer update mode.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some of these should mismatch on purpose to ensure the mapping is working
|
* Some of these should mismatch on purpose to ensure the mapping is working
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Importing;
|
||||||
|
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\AssetModel;
|
||||||
|
use App\Models\Category;
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\Location;
|
||||||
|
use App\Models\Statuslabel;
|
||||||
|
use App\Models\User;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AssetImportCreatedAtTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Test that importing assets doesn't modify created_at timestamps on existing assets
|
||||||
|
* This test addresses the reported bug where large imports caused random created_at changes
|
||||||
|
*/
|
||||||
|
#[Test]
|
||||||
|
public function existing_asset_created_at_not_modified_on_import_update()
|
||||||
|
{
|
||||||
|
// Create test data
|
||||||
|
$category = Category::factory()->create(['category_type' => 'asset']);
|
||||||
|
$location = Location::factory()->create();
|
||||||
|
$statusLabel = Statuslabel::factory()->create();
|
||||||
|
$company = Company::factory()->create();
|
||||||
|
$assetModel = AssetModel::factory()->for($category, 'category')->create();
|
||||||
|
|
||||||
|
// Create an existing asset with a known created_at date
|
||||||
|
$originalCreatedAt = now()->subDays(30)->toDateTimeString();
|
||||||
|
$asset = Asset::factory()
|
||||||
|
->for($assetModel, 'model')
|
||||||
|
->for($statusLabel, 'status')
|
||||||
|
->for($location, 'defaultLoc')
|
||||||
|
->for($company, 'company')
|
||||||
|
->create([
|
||||||
|
'asset_tag' => 'TEST-001',
|
||||||
|
'name' => 'Test Asset',
|
||||||
|
'created_at' => $originalCreatedAt,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create CSV content that updates the existing asset
|
||||||
|
$csv = "asset tag,item name,category,status\n";
|
||||||
|
$csv .= "TEST-001,Test Asset Updated,{$category->name},{$statusLabel->name}\n";
|
||||||
|
|
||||||
|
// Perform import with update flag
|
||||||
|
$this->actingAsForApi(User::factory()->canImport()->create())
|
||||||
|
->postJson(route('api.imports.store'), [
|
||||||
|
'files' => [
|
||||||
|
$this->createFakeUploadedFile('test.csv', $csv),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
// Import the file
|
||||||
|
$import = \App\Models\Import::latest()->first();
|
||||||
|
$this->actingAsForApi(User::factory()->canImport()->create())
|
||||||
|
->postJson(route('api.imports.importFile', $import->id), [
|
||||||
|
'import-type' => 'asset',
|
||||||
|
'import-update' => true,
|
||||||
|
'column-mappings' => [
|
||||||
|
'asset tag' => 'asset_tag',
|
||||||
|
'item name' => 'item_name',
|
||||||
|
'category' => 'category',
|
||||||
|
'status' => 'status',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
// Verify the asset's created_at timestamp wasn't modified
|
||||||
|
$asset->refresh();
|
||||||
|
$this->assertEquals(
|
||||||
|
$originalCreatedAt,
|
||||||
|
$asset->created_at->toDateTimeString(),
|
||||||
|
'Asset created_at timestamp was modified during import update, which should not happen'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the asset was updated correctly
|
||||||
|
$this->assertEquals('Test Asset Updated', $asset->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that multiple successive imports don't cause timestamp drift
|
||||||
|
*/
|
||||||
|
#[Test]
|
||||||
|
public function successive_imports_maintain_created_at_consistency()
|
||||||
|
{
|
||||||
|
// Create test data
|
||||||
|
$category = Category::factory()->create(['category_type' => 'asset']);
|
||||||
|
$location = Location::factory()->create();
|
||||||
|
$statusLabel = Statuslabel::factory()->create();
|
||||||
|
$company = Company::factory()->create();
|
||||||
|
$assetModel = AssetModel::factory()->for($category, 'category')->create();
|
||||||
|
|
||||||
|
// Create multiple existing assets
|
||||||
|
$assets = collect();
|
||||||
|
for ($i = 1; $i <= 5; $i++) {
|
||||||
|
$assets->push(Asset::factory()
|
||||||
|
->for($assetModel, 'model')
|
||||||
|
->for($statusLabel, 'status')
|
||||||
|
->for($location, 'defaultLoc')
|
||||||
|
->for($company, 'company')
|
||||||
|
->create([
|
||||||
|
'asset_tag' => "TEST-{$i}",
|
||||||
|
'created_at' => now()->subDays(30 - $i),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalCreatedAts = $assets->mapWithKeys(fn ($asset) => [$asset->id => $asset->created_at->toDateTimeString()])->toArray();
|
||||||
|
|
||||||
|
// Perform first import update
|
||||||
|
$csv = "asset tag,item name,category,status\n";
|
||||||
|
foreach ($assets as $asset) {
|
||||||
|
$csv .= "{$asset->asset_tag},{$asset->name},{$category->name},{$statusLabel->name}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->actingAsForApi(User::factory()->canImport()->create())
|
||||||
|
->postJson(route('api.imports.store'), [
|
||||||
|
'files' => [
|
||||||
|
$this->createFakeUploadedFile('test1.csv', $csv),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
$import1 = \App\Models\Import::latest()->first();
|
||||||
|
$this->actingAsForApi(User::factory()->canImport()->create())
|
||||||
|
->postJson(route('api.imports.importFile', $import1->id), [
|
||||||
|
'import-type' => 'asset',
|
||||||
|
'import-update' => true,
|
||||||
|
'column-mappings' => [
|
||||||
|
'asset tag' => 'asset_tag',
|
||||||
|
'item name' => 'item_name',
|
||||||
|
'category' => 'category',
|
||||||
|
'status' => 'status',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
// Perform second import update
|
||||||
|
$this->actingAsForApi(User::factory()->canImport()->create())
|
||||||
|
->postJson(route('api.imports.store'), [
|
||||||
|
'files' => [
|
||||||
|
$this->createFakeUploadedFile('test2.csv', $csv),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
$import2 = \App\Models\Import::latest()->first();
|
||||||
|
$this->actingAsForApi(User::factory()->canImport()->create())
|
||||||
|
->postJson(route('api.imports.importFile', $import2->id), [
|
||||||
|
'import-type' => 'asset',
|
||||||
|
'import-update' => true,
|
||||||
|
'column-mappings' => [
|
||||||
|
'asset tag' => 'asset_tag',
|
||||||
|
'item name' => 'item_name',
|
||||||
|
'category' => 'category',
|
||||||
|
'status' => 'status',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
|
// Verify all assets' created_at timestamps remain unchanged
|
||||||
|
foreach ($assets as $asset) {
|
||||||
|
$asset->refresh();
|
||||||
|
$this->assertEquals(
|
||||||
|
$originalCreatedAts[$asset->id],
|
||||||
|
$asset->created_at->toDateTimeString(),
|
||||||
|
"Asset {$asset->asset_tag} created_at changed between imports"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create a fake uploaded file
|
||||||
|
*/
|
||||||
|
protected function createFakeUploadedFile(string $filename, string $content)
|
||||||
|
{
|
||||||
|
$path = tempnam(sys_get_temp_dir(), 'csv');
|
||||||
|
file_put_contents($path, $content);
|
||||||
|
|
||||||
|
return new \Illuminate\Http\UploadedFile(
|
||||||
|
$path,
|
||||||
|
$filename,
|
||||||
|
'text/csv',
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user