'Laravel resource API does not load the updated version of relation data on update endpoint
I have Transaction model. In that model, there is a quantity field and a foodstuff relationship.
Then, inside foodstuff there is a stock field.
I was expecting that if I update the quantity in Transaction model, it should also update the stock in the foodstuff. The logic is written in the model and called inside the controller. Please read the commented codes, I put an explanation there to be clear.
TransactionType.php
<?php
namespace App\Enums;
use BenSampo\Enum\Enum;
/**
* @method static static in()
* @method static static out()
* @method static static cancelled()
* @method static static returned()
*/
final class TransactionType extends Enum
{
const in = 'in';
const out = 'out';
const cancelled = 'cancelled';
const returned = 'returned';
}
Transaction.php
<?php
namespace App\Models;
use App\Enums\TransactionType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Transaction extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'foodstuff_id',
'type',
'quantity',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'foodstuff_id' => 'int',
'quantity' => 'int',
];
/**
* Update foodstuff stock based on the given transaction type.
*
* @param string $type
* @return void
*/
public function updateStock(string $type): void
{
$currentStock = $this->foodstuff->stock;
$quantity = $this->quantity;
$type == TransactionType::in
? $this->foodstuff->update(['stock' => $currentStock + $quantity])
: $this->foodstuff->update(['stock' => $currentStock - $quantity]);
}
/**
* Get the foodstuff that owns the Transaction
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function foodstuff(): BelongsTo
{
return $this->belongsTo(Foodstuff::class);
}
}
TransactionController.php
public function store(StoreTransactionRequest $request)
{
$record = Transaction::create($request->validated());
$record->updateStock($request->type);
// This works fine, test passed
return new TransactionResource($record->refresh());
}
public function update(UpdateTransactionRequest $request, Transaction $transaction)
{
$oldStock = $transaction->foodstuff->stock;
$transaction->update($request->validated());
$transaction->updateStock($request->type);
$newStock = $transaction->foodstuff->stock;
// dd(
// $oldStock, // 47
// $newStock, // 48
//
// // to confirm that the operation is working properly
// $request->type, // "in"
// $request->quantity, // 1
// $oldStock + $request->quantity, // 48
// $oldStock + $transaction->quantity, // 48
// );
// This one still using the old stock (47), eventhough I refreshed it.
// Test fails,
// Failed asserting that 47 matches expected 48.
$transaction->foodstuff->refresh();
return new TransactionResource($transaction->refresh());
}
TransactionResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class TransactionResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
...
...
...
'foodstuff' => [
'stock' => $this->foodstuff->stock,
...
...
],
];
}
}
Feature/TransactionTest.php
// A test for the store endpoint
public function testWhenUserStoreNewRecordWithTypeOfInItShouldUpdateTheFoodstuffStock()
{
Sanctum::actingAs(User::factory()->createOne());
$foodstuff = Foodstuff::factory()->createOne();
$record = Transaction::factory()
->state(['foodstuff_id' => $foodstuff->id, 'type' => TransactionType::in])
->makeOne()
->toArray();
$response = $this->postJson(route('transactions.store'), $record);
$response->assertCreated();
$response->assertJsonFragment($record);
$data = $response->decodeResponseJson();
$computedStock = $foodstuff->stock + $record['quantity'];
$this->assertEquals($computedStock, $data['data']['foodstuff']['stock']);
}
// A test for update endpoint
public function testWhenUserUpdateNewRecordWithTypeOfInItShouldUpdateTheFoodstuffStock()
{
Sanctum::actingAs(User::factory()->createOne());
$oldRecord = Transaction::factory()->createOne();
$newRecord = Transaction::factory()
->state(['foodstuff_id' => $oldRecord->foodstuff_id, 'type' => TransactionType::in])
->makeOne()
->toArray();
$response = $this->putJson(route('transactions.update', $oldRecord), $newRecord);
$response->assertOk();
$response->assertJsonFragment($newRecord);
$data = $response->decodeResponseJson();
$computedStock = $oldRecord->foodstuff->stock + $newRecord['quantity'];
// dd(
// $oldRecord->foodstuff->stock, // 47
// $data['data']['foodstuff']['stock'], // 47
// $computedStock, // 48
// );
$this->assertEquals($computedStock, $data['data']['foodstuff']['stock']);
}
Test Result
FAIL Tests\Feature\TransactionTest
✓ when user store new record with type of in it should update the foodstuff stock
⨯ when user update new record with type of in it should update the foodstuff stock
---
• Tests\Feature\TransactionTest > when user update new record with type of in it should update the foodstuff stock
Failed asserting that 47 matches expected 48.
at tests/Feature/TransactionTest.php:328
324▕ // $data['data']['foodstuff']['stock'],
325▕ // $computedStock,
326▕ // );
327▕
➜ 328▕ $this->assertEquals($computedStock, $data['data']['foodstuff']['stock']);
329▕ }
330▕
331▕ public function testWhenUserUpdateNewRecordWithTypeOfOutItShouldUpdateTheFoodstuffStock()
332▕ {
Tests: 1 failed
Time: 0.62s
Why is this happening and how can I make it load the updated version of foodstuff?
Solution 1:[1]
I tried to reproduce your code and they are all working fine except for your testing logic.
$this->assertEquals($computedStock, $data['data']['foodstuff']['stock']);
$data['data']['foodstuff']['stock'] // this one is already returning updated data.
// You're testing like
$this->assertEquals($updatedQuantity + 1, $updatedQuantity);
That's why your test failed not because of your code but because of your test logic.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | sithuaung |
