mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
And more tests
This commit is contained in:
@ -50,7 +50,7 @@ class AdjustAccessoryQuantityApiTest extends TestCase
|
||||
$this->assertSame('restock from PO', $log->note);
|
||||
// order_number moved off the log row to the Orders table — the
|
||||
// log carries order_id pointing at the newly-created Order.
|
||||
$order = \App\Models\Order::findOrFail($log->order_id);
|
||||
$order = $log->orderItem->order;
|
||||
$this->assertSame('PO-API-1', $order->order_number);
|
||||
$this->assertSame($actor->id, (int) $log->created_by);
|
||||
}
|
||||
@ -163,4 +163,118 @@ class AdjustAccessoryQuantityApiTest extends TestCase
|
||||
->count(), 'No separate uploaded log entry should have been created.');
|
||||
$this->assertSame($uploadsBefore + 1, $accessory->uploads()->count());
|
||||
}
|
||||
|
||||
public function test_supplier_purchase_date_unit_cost_and_currency_land_on_order_and_order_item()
|
||||
{
|
||||
$accessory = Accessory::factory()->create(['qty' => 2]);
|
||||
$supplier = \App\Models\Supplier::factory()->create();
|
||||
|
||||
$this->actingAsForApi(User::factory()->editAccessories()->create())
|
||||
->postJson(route('api.accessories.adjust-quantity', $accessory), [
|
||||
'amount' => 5,
|
||||
'note' => 'PO arrived',
|
||||
'order_number' => 'PO-FULL-META',
|
||||
'supplier_id' => $supplier->id,
|
||||
'purchase_date' => '2026-03-15',
|
||||
'unit_cost' => 12.3456,
|
||||
'currency' => 'EUR',
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$log = Actionlog::where('item_type', Accessory::class)
|
||||
->where('item_id', $accessory->id)
|
||||
->where('action_type', ActionType::QuantityAdjust->value)
|
||||
->latest('id')
|
||||
->firstOrFail();
|
||||
|
||||
$order = $log->orderItem->order;
|
||||
$this->assertSame('PO-FULL-META', $order->order_number);
|
||||
$this->assertSame($supplier->id, (int) $order->supplier_id);
|
||||
$this->assertSame('2026-03-15', $order->purchase_date->toDateString());
|
||||
$this->assertSame('EUR', $order->currency);
|
||||
|
||||
$line = $order->orderItems()->firstOrFail();
|
||||
$this->assertSame(Accessory::class, $line->item_type);
|
||||
$this->assertSame($accessory->id, (int) $line->item_id);
|
||||
$this->assertSame(5, (int) $line->qty);
|
||||
$this->assertEquals(12.3456, (float) $line->price);
|
||||
}
|
||||
|
||||
public function test_audit_only_zero_delta_with_no_order_metadata_skips_order_creation()
|
||||
{
|
||||
// Zero delta and no supplier / order number / date / cost /
|
||||
// currency = pure inventory audit. No Order row should be
|
||||
// created for these, since there was no transaction.
|
||||
$accessory = Accessory::factory()->create(['qty' => 5]);
|
||||
$ordersBefore = \App\Models\Order::count();
|
||||
|
||||
$this->actingAsForApi(User::factory()->editAccessories()->create())
|
||||
->postJson(route('api.accessories.adjust-quantity', $accessory), [
|
||||
'amount' => 0,
|
||||
'note' => 'shelf count matches',
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$this->assertSame($ordersBefore, \App\Models\Order::count());
|
||||
|
||||
$log = Actionlog::where('item_type', Accessory::class)
|
||||
->where('item_id', $accessory->id)
|
||||
->where('action_type', ActionType::QuantityAdjust->value)
|
||||
->latest('id')
|
||||
->firstOrFail();
|
||||
|
||||
$this->assertNull($log->order_item_id);
|
||||
}
|
||||
|
||||
public function test_multiple_adjusts_with_same_order_number_dedupe_to_one_order()
|
||||
{
|
||||
// Repeated qty adjusts referencing the same order_number should
|
||||
// reuse the existing Order row (dedupe on order_number +
|
||||
// supplier + company), while each adjust still produces its own
|
||||
// OrderItem line (one line per event).
|
||||
$accessory = Accessory::factory()->create(['qty' => 0]);
|
||||
$supplier = \App\Models\Supplier::factory()->create();
|
||||
$actor = User::factory()->editAccessories()->create();
|
||||
|
||||
foreach ([3, 4] as $delta) {
|
||||
$this->actingAsForApi($actor)
|
||||
->postJson(route('api.accessories.adjust-quantity', $accessory), [
|
||||
'amount' => $delta,
|
||||
'note' => 'staggered receipt',
|
||||
'order_number' => 'PO-DEDUPE',
|
||||
'supplier_id' => $supplier->id,
|
||||
])
|
||||
->assertOk();
|
||||
}
|
||||
|
||||
$orders = \App\Models\Order::where('order_number', 'PO-DEDUPE')->get();
|
||||
$this->assertCount(1, $orders, 'Same order_number should reuse the existing Order row.');
|
||||
|
||||
$items = $orders->first()->orderItems;
|
||||
$this->assertCount(2, $items, 'Each adjust event should have its own OrderItem line.');
|
||||
$this->assertEqualsCanonicalizing([3, 4], $items->pluck('qty')->map(fn ($q) => (int) $q)->all());
|
||||
}
|
||||
|
||||
public function test_blank_order_number_creates_a_distinct_order_per_event()
|
||||
{
|
||||
// A blank order_number is a distinct transaction each time, not
|
||||
// a bucket to pool anonymous receipts into. Each adjust with
|
||||
// no order_number gets its own Order row.
|
||||
$accessory = Accessory::factory()->create(['qty' => 0]);
|
||||
$supplier = \App\Models\Supplier::factory()->create();
|
||||
$actor = User::factory()->editAccessories()->create();
|
||||
$ordersBefore = \App\Models\Order::count();
|
||||
|
||||
foreach ([2, 3] as $delta) {
|
||||
$this->actingAsForApi($actor)
|
||||
->postJson(route('api.accessories.adjust-quantity', $accessory), [
|
||||
'amount' => $delta,
|
||||
'note' => 'anonymous receipt',
|
||||
'supplier_id' => $supplier->id,
|
||||
])
|
||||
->assertOk();
|
||||
}
|
||||
|
||||
$this->assertSame($ordersBefore + 2, \App\Models\Order::count());
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,7 +185,7 @@ class UpdateAccessoryTest extends TestCase implements TestsFullMultipleCompanies
|
||||
->firstOrFail();
|
||||
|
||||
$this->assertSame(7, (int) $log->quantity);
|
||||
$this->assertSame('PO-API', \App\Models\Order::findOrFail($log->order_id)->order_number);
|
||||
$this->assertSame('PO-API', $log->orderItem->order->order_number);
|
||||
$this->assertNotEmpty($log->note, 'Adjustment note must be synthesized when the API caller omits one');
|
||||
}
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ class AdjustAccessoryQuantityTest extends TestCase
|
||||
$this->assertNotNull($log);
|
||||
$this->assertSame(3, (int) $log->quantity);
|
||||
$this->assertSame('restock from PO', $log->note);
|
||||
$this->assertSame('PO-42', \App\Models\Order::findOrFail($log->order_id)->order_number);
|
||||
$this->assertSame('PO-42', $log->orderItem->order->order_number);
|
||||
$this->assertSame($actor->id, (int) $log->created_by);
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ class AdjustAccessoryQuantityTest extends TestCase
|
||||
->first();
|
||||
|
||||
$this->assertSame(-4, (int) $log->quantity);
|
||||
$this->assertNull($log->order_id);
|
||||
$this->assertNull($log->order_item_id);
|
||||
}
|
||||
|
||||
public function test_decrement_below_zero_is_rejected()
|
||||
|
||||
@ -74,10 +74,7 @@ class StoreAssetTest extends TestCase
|
||||
$this->assertTrue($asset->model->is($model));
|
||||
$this->assertEquals('A New Asset', $asset->name);
|
||||
$this->assertEquals('Some notes', $asset->notes);
|
||||
// order_number moved off the parent Asset column to the Orders /
|
||||
// OrderItems data model. A create-time order_number in the request
|
||||
// body no longer lands on the model — acquisition tracking is via
|
||||
// the adjust-quantity flow or the CSV importer.
|
||||
$this->assertEquals('5678', $asset->order_number);
|
||||
$this->assertEquals('123.45', $asset->purchase_cost);
|
||||
$this->assertTrue($asset->purchase_date->is('2023-09-02'));
|
||||
$this->assertEquals('1', $asset->requestable);
|
||||
|
||||
@ -95,8 +95,7 @@ class UpdateAssetTest extends TestCase
|
||||
$this->assertTrue($updatedAsset->model->is($model));
|
||||
$this->assertEquals('A New Asset', $updatedAsset->name);
|
||||
$this->assertEquals('Some notes', $updatedAsset->notes);
|
||||
// See StoreAssetTest for why order_number is no longer on the
|
||||
// parent Asset column.
|
||||
$this->assertEquals('5678', $updatedAsset->order_number);
|
||||
$this->assertEquals('123.45', $updatedAsset->purchase_cost);
|
||||
$this->assertTrue($updatedAsset->purchase_date->is('2023-09-02'));
|
||||
$this->assertEquals('1', $updatedAsset->requestable);
|
||||
|
||||
102
tests/Feature/Assets/AssetObserverOrderTest.php
Normal file
102
tests/Feature/Assets/AssetObserverOrderTest.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Assets;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* AssetObserver::created writes a matching Order + OrderItem for every
|
||||
* new asset regardless of how the asset arrived (web form, API, CSV
|
||||
* importer). Dedupe semantics match the adjust-quantity flow: a
|
||||
* populated order_number shares the Order row with same-tuple assets,
|
||||
* a blank one gets its own Order per event.
|
||||
*/
|
||||
class AssetObserverOrderTest extends TestCase
|
||||
{
|
||||
public function test_asset_create_with_order_number_writes_matching_order_and_order_item(): void
|
||||
{
|
||||
$actor = User::factory()->createAssets()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
|
||||
$asset = Asset::factory()->create([
|
||||
'created_by' => $actor->id,
|
||||
'company_id' => $actor->company_id,
|
||||
'supplier_id' => $supplier->id,
|
||||
'order_number' => 'PO-OBS-1',
|
||||
'purchase_date' => '2026-04-01',
|
||||
'purchase_cost' => 199.99,
|
||||
]);
|
||||
|
||||
$line = OrderItem::where('item_type', Asset::class)
|
||||
->where('item_id', $asset->id)
|
||||
->firstOrFail();
|
||||
|
||||
$this->assertSame(1, (int) $line->qty);
|
||||
$this->assertEquals(199.99, (float) $line->price);
|
||||
|
||||
$order = Order::findOrFail($line->order_id);
|
||||
$this->assertSame('PO-OBS-1', $order->order_number);
|
||||
$this->assertSame($supplier->id, (int) $order->supplier_id);
|
||||
$this->assertSame($actor->company_id, $order->company_id);
|
||||
$this->assertSame('2026-04-01', $order->purchase_date->toDateString());
|
||||
}
|
||||
|
||||
public function test_two_assets_with_same_order_tuple_share_one_order_row(): void
|
||||
{
|
||||
$actor = User::factory()->createAssets()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
|
||||
$shared = [
|
||||
'created_by' => $actor->id,
|
||||
'company_id' => $actor->company_id,
|
||||
'supplier_id' => $supplier->id,
|
||||
'order_number' => 'PO-OBS-SHARED',
|
||||
'purchase_date' => '2026-04-02',
|
||||
];
|
||||
|
||||
$first = Asset::factory()->create($shared + ['purchase_cost' => 100]);
|
||||
$second = Asset::factory()->create($shared + ['purchase_cost' => 200]);
|
||||
|
||||
$orders = Order::where('order_number', 'PO-OBS-SHARED')
|
||||
->where('supplier_id', $supplier->id)
|
||||
->where('company_id', $actor->company_id)
|
||||
->get();
|
||||
|
||||
$this->assertCount(1, $orders, 'Same (order_number, supplier, company) tuple must dedupe.');
|
||||
|
||||
$lineIds = OrderItem::where('item_type', Asset::class)
|
||||
->whereIn('item_id', [$first->id, $second->id])
|
||||
->pluck('order_id')
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$this->assertCount(1, $lineIds);
|
||||
$this->assertSame($orders->first()->id, (int) $lineIds->first());
|
||||
}
|
||||
|
||||
public function test_blank_order_number_creates_a_distinct_order_per_asset(): void
|
||||
{
|
||||
$actor = User::factory()->createAssets()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
$ordersBefore = Order::count();
|
||||
|
||||
$shared = [
|
||||
'created_by' => $actor->id,
|
||||
'company_id' => $actor->company_id,
|
||||
'supplier_id' => $supplier->id,
|
||||
'order_number' => null,
|
||||
];
|
||||
|
||||
Asset::factory()->create($shared);
|
||||
Asset::factory()->create($shared);
|
||||
|
||||
// Blank order_number = distinct transaction, so both assets get
|
||||
// their own Order rows even though supplier + company match.
|
||||
$this->assertSame($ordersBefore + 2, Order::count());
|
||||
}
|
||||
}
|
||||
@ -46,7 +46,7 @@ class AdjustComponentQuantityApiTest extends TestCase
|
||||
->firstOrFail();
|
||||
|
||||
$this->assertSame(3, (int) $log->quantity);
|
||||
$this->assertSame('PO-CMP', \App\Models\Order::findOrFail($log->order_id)->order_number);
|
||||
$this->assertSame('PO-CMP', $log->orderItem->order->order_number);
|
||||
}
|
||||
|
||||
public function test_note_is_required()
|
||||
|
||||
Reference in New Issue
Block a user