Fixed FD-52058 - Importer using wrong created_at date

This commit is contained in:
snipe
2026-05-13 12:06:13 +01:00
parent 529973aa77
commit 96bf7d0c2b
15 changed files with 593 additions and 14 deletions
+2 -1
View File
@@ -37,7 +37,8 @@ class AccessoryImporter extends ItemImporter
$this->log('Updating Accessory');
$this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
$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;
}
+28 -8
View File
@@ -176,35 +176,55 @@ class AssetImporter extends ItemImporter
if ($editingAsset) {
$asset->update($item);
$asset->setImported(true);
} else {
$asset->fill($item);
$asset->setImported(true);
}
// 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)) {
foreach ($this->item['custom_fields'] as $custom_field => $val) {
$asset->{$custom_field} = $val;
$customFieldsToSave[$custom_field] = $val;
}
}
// This sets an attribute on the Loggable trait for the action log
$asset->setImported(true);
// For existing assets that have custom fields, update them.
// 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.
// -- 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).
if (isset($target) && ($target !== false)) {
if (! is_null($asset->assigned_to)) {
if ($asset->assigned_to != $target->id) {
$asset = $asset->fresh();
$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));
}
}
$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;
+2 -1
View File
@@ -42,7 +42,8 @@ class ComponentImporter extends ItemImporter
}
$this->log('Updating 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;
}
+2 -1
View File
@@ -38,7 +38,8 @@ class ConsumableImporter extends ItemImporter
}
$this->log('Updating 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;
}
+6 -2
View File
@@ -88,8 +88,12 @@ class LicenseImporter extends ItemImporter
// This sets an attribute on the Loggable trait for the action log
$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.
if ($license->seats > 0) {
+1 -1
View File
@@ -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
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
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
1 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
2 Abshire and Sons 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
3 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
4 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
5 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]
public function when_import_file_contains_empty_values(): void
{
@@ -2,6 +2,7 @@
namespace Tests\Feature\Importing\Api;
use App\Models\Actionlog as ActionLog;
use App\Models\AssetModel;
use App\Models\Category;
use App\Models\Import;
@@ -131,4 +132,46 @@ class ImportAssetModelsTest extends ImportDataTestCase implements TestsPermissio
$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]
public function custom_column_mapping(): void
{
@@ -249,6 +249,52 @@ class ImportComponentsTest extends ImportDataTestCase implements TestsPermission
$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]
public function custom_column_mapping(): void
{
@@ -243,6 +243,49 @@ class ImportConsumablesTest extends ImportDataTestCase implements TestsPermissio
$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]
public function custom_column_mapping(): void
{
@@ -277,6 +277,52 @@ class ImportLicenseTest extends ImportDataTestCase implements TestsPermissionsRe
$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]
public function custom_column_mapping(): void
{
@@ -2,6 +2,7 @@
namespace Tests\Feature\Importing\Api;
use App\Models\Actionlog as ActionLog;
use App\Models\Import;
use App\Models\Location;
use App\Models\User;
@@ -129,4 +130,46 @@ class ImportLocationsTest extends ImportDataTestCase implements TestsPermissions
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;
use App\Models\Actionlog as ActionLog;
use App\Models\Asset;
use App\Models\Import;
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
*/
@@ -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
);
}
}