I am making an inventory management system. When a product is out of stock, I make an entry in a table and note the "oos_at" field with the date/time.
later, when it is back in stock, i find that entry and update the "restocked_at" timestamp field.
However, when I do the second action, my "oos_at" field is overwritten with the timestamp of the query ('updated_at') field... I had to keep track of this "oos_at" field on another entity so it wouldn't get overwritten and then use that field to update the "oos_at" field a second time when updating that table...
Below is my code....
class Pusher extends Model {
/**
 *
 *
 * @param Carbon $oos_at
 * @return bool
 */
public function setAsOutOfStock(Carbon $oos_at)
{
    Log::info([
        'FILE'  => get_class($this),
        'method'   => 'setAsOutOfStock',
        'pusher'    => $this->id,
        'param:oos_at'  => $oos_at->toDateTimeString(),
    ]);
    $this->pusherOutOfStocks()->create([
        'location_id'   => $this->location_id,
        'product_id'    => $this->product_id,
        'oos_at'        => $oos_at->toDateTimeString(),
    ]);
    $this->oos = true;
    $this->oos_at = $oos_at;
    return $this->save();
}
/**
 * Clear the PusherOutOfStocks attached to $this Pusher
 * (This Pusher has been restocked!)
 *
 * @param Carbon $time
 * @return bool
 */
public function setAsInStock(Carbon $time)
{
    Log::info([
        'FILE'      => get_class($this),
        'method'    => 'setAsInStock',
        'pusher'    => $this->id,
        'param:time' => $time->toDateTimeString(),
    ]);
    $this->pusherOutOfStocks()->where('restocked_at', null)
        ->update(['restocked_at' => $time->toDateTimeString()]);
    $this->oos = false;
    $this->oos_at = null;
    return $this->save();
}
}
When I die and dump the PusherOutOfStocks BEFORE the pusher is restocked, then the "oos_at" is set appropriately.
Illuminate\Database\Eloquent\Collection {#340
  #items: array:1 [
    0 => Newave\Engineering\PusherOutOfStock {#343
      #fillable: array:5 [
        0 => "pusher_id"
        1 => "location_id"
        2 => "product_id"
        3 => "oos_at"
        4 => "restocked_at"
      ]
      #dates: array:2 [
        0 => "oos_at"
        1 => "restocked_at"
      ]
      #connection: null
      #table: null
      #primaryKey: "id"
      #perPage: 15
      +incrementing: true
      +timestamps: true
      #attributes: array:9 [
        "id" => 246
        "pusher_id" => 216
        "location_id" => 634
        "product_id" => 378
        "oos_at" => "2016-03-11 03:00:00"
        "restocked_at" => null
        "created_at" => "2016-03-11 12:12:01"
        "updated_at" => "2016-03-11 12:12:01"
        "deleted_at" => null
      ]
      #original: array:9 [
        "id" => 246
        "pusher_id" => 216
        "location_id" => 634
        "product_id" => 378
        "oos_at" => "2016-03-11 03:00:00"
        "restocked_at" => null
        "created_at" => "2016-03-11 12:12:01"
        "updated_at" => "2016-03-11 12:12:01"
        "deleted_at" => null
      ]
      #relations: []
      #hidden: []
      #visible: []
      #appends: []
      #guarded: array:1 [
        0 => "*"
      ]
      #dateFormat: null
      #casts: []
      #touches: []
      #observables: []
      #with: []
      #morphClass: null
      +exists: true
      +wasRecentlyCreated: false
      #forceDeleting: false
    }
  ]
}
When I die and dump the PusherOutOfStocks AFTER the pusher is restocked, then the "oos_at" is the same as "updated_at"
Illuminate\Database\Eloquent\Collection {#408
  #items: array:1 [
    0 => Newave\Engineering\PusherOutOfStock {#775
      #fillable: array:5 [
        0 => "pusher_id"
        1 => "location_id"
        2 => "product_id"
        3 => "oos_at"
        4 => "restocked_at"
      ]
      #dates: array:2 [
        0 => "oos_at"
        1 => "restocked_at"
      ]
      #connection: null
      #table: null
      #primaryKey: "id"
      #perPage: 15
      +incrementing: true
      +timestamps: true
      #attributes: array:9 [
        "id" => 244
        "pusher_id" => 214
        "location_id" => 626
        "product_id" => 374
        "oos_at" => "2016-03-11 12:10:23"
        "restocked_at" => "2016-03-11 04:00:00"
        "created_at" => "2016-03-11 12:10:22"
        "updated_at" => "2016-03-11 12:10:23"
        "deleted_at" => null
      ]
      #original: array:9 [
        "id" => 244
        "pusher_id" => 214
        "location_id" => 626
        "product_id" => 374
        "oos_at" => "2016-03-11 12:10:23"
        "restocked_at" => "2016-03-11 04:00:00"
        "created_at" => "2016-03-11 12:10:22"
        "updated_at" => "2016-03-11 12:10:23"
        "deleted_at" => null
      ]
      #relations: []
      #hidden: []
      #visible: []
      #appends: []
      #guarded: array:1 [
        0 => "*"
      ]
      #dateFormat: null
      #casts: []
      #touches: []
      #observables: []
      #with: []
      #morphClass: null
      +exists: true
      +wasRecentlyCreated: false
      #forceDeleting: false
    }
  ]
}
I have even used DB::getQueryLog() and saw NOWHERE that 'oos_at' was being explicitly set/updated....
Nowhere else in my code is this table modified...
Does anyone understand what is happening here???? Thank you!!
=============
Extra Code Snippets
Table Migration::
    Schema::create('pusher_out_of_stocks', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('pusher_id');
        $table->integer('location_id');
        $table->integer('product_id');
        $table->timestamp('oos_at');
        $table->timestamp('restocked_at')->nullable();
        $table->timestamps();
        $table->softDeletes();
    });
PusherOutOfStock Class
class PusherOutOfStock extends Model
{
    use SoftDeletes;
    protected $fillable = [
        'pusher_id',
        'location_id',
        'product_id',
        'oos_at',
        'restocked_at',
    ];
    protected $dates = [
        'oos_at',
        'restocked_at'
    ];
    /**
     * A PusherOutOfStock belongsTo a Product
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function product()
    {
        return $this->belongsTo(Product::class);
    }
    /**
     * A PusherOutOfStock belongsTo a Pusher
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function pusher()
    {
        return $this->belongsTo(Pusher::class);
    }
    /**
     * A PusherOutOfStock belongsTo a Location
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function location()
    {
        return $this->belongsTo(Location::class);
    }
}
Below is the sequence the code goes through to hit that update method
First, it hits my InventoryController in my API namespace
    public function upload(Request $request)
    {
        $data = $request->all();
        $header = $this->getHeader($data);
        $body   = $this->getBody($data);
        $reader = $this->getReaderFromHeader($header);
        if ( ! $this->guardAgainstNoReader($reader))
            return (new Response("Reader with mac-address: ". $header['reader_mac'] . " does not exist.", 409));
        $result = $this->inventoryRepository->uploadPusherData($reader, $header, $body);
        return $result;
    }
Then it sends the request to the InventoryRepository
public function uploadPusherData(Reader $reader, $header, $body)
{
    foreach ($body as $pusherData)
    {
        $result[] = $this->processPusherData($pusher, $header['timestamp'], $pusherData);
    }
    return $result;
}
Then, inside the repository, it processes one line at a time (in my test, there is only one line)
private function processPusherData(Pusher $pusher, $timestamp, $pusherData)
{
    $latestInventory = $pusher->latestInventory;
    if (! $latestInventory)
        return $this->updatePusher($pusher, $timestamp, $pusherData);
    if ($latestInventory->tags_blocked == $pusherData['data_TAGSBLKED'])
        return $this->noChangeRecorded($pusher, $latestInventory, $timestamp);
    return $this->updatePusher($pusher, $timestamp, $pusherData);
}
Then the pusher is updated...
public function updatePusher($pusher, $timestamp, $pusherData)
{
    // See if there are any existing already
    $prevInv = $pusher->latestInventory;
    // Create the new data
    $inventory = Inventory::create([
        'pusher_id'     => $pusher->id,
        'product_id'    => $pusher->product_id,
        'reader_id'     => $pusher->reader->id,
        'tags_blocked'  => $pusherData['data_TAGSBLKED'],
        'paddle_exposed'=> $pusherData['paddle_exposed'],
        'created_at'    => Carbon::createFromTimestamp($timestamp)
                            ->toDateTimeString(),
    ]);
    if (  !$prevInv || $prevInv->id == $inventory->id )
    {
        return "first-data" . $timestamp;
    }
    return $this->checkForEvents($inventory, $prevInv);
}
We check if any events should be triggered... In this case, previous inventory had 9 items in stock... now there are 0.
private function checkForEvents(Inventory $currentInventory, Inventory $previousInventory)
{
    if ( ! $previousInventory->oos && $currentInventory->oos && $previousInventory->pusher->oos_notified == 0)
    {
        $currentInventory->pusher->oos_notified = true;
        $currentInventory->pusher->save();
        return Event::fire(new InventoryOutOfStock($currentInventory));
    }
    if ( ( $previousInventory->oos || $previousInventory->status == "RESTOCK" )
            && $currentInventory->tags_blocked > 2 )
    {
        return Event::fire(new PusherWasRestocked($currentInventory));
    }
    if ( $currentInventory->status == "RESTOCK" && $previousInventory->pusher->low_stock_notified == 0)
    {
        $currentInventory->pusher->low_stock_notified = true;
        $currentInventory->pusher->save();
        return Event::fire(new LowStockAlert($currentInventory));
    }
    return "no-events";
}
This then fires the event InventoryOutOfStock
That triggers 3 events... 2 are related to notifications being sent etc..
    'App\Events\InventoryOutOfStock' => [
        'App\Listeners\InventoryOutOfStockUpdater',
        'App\Listeners\EmailInventoryOutOfStockNotification',
        'App\Listeners\SMSInventoryOutOfStockNotification',
// 'App\Listeners\OutOfStocksUpdater', ],
Which leads us to ...
public function handle(InventoryOutOfStock $event)
{
    $pusher = $event->pusher;
    $inventory = $event->inventory;
    $product = $pusher->product;
    $oos = $pusher->setAsOutOfStock($inventory->created_at);
    $locationPushers = $product->getPushersByLocation($pusher->location);
    $isInStock = false;
    foreach ($locationPushers as $pusher)
        if ($pusher->oos == 0)
            $isInStock = true;
    if (! $isInStock)
        $product->productOutOfStocks()->create([
            'location_id'   => $pusher->location_id,
            'oos_at'        => $event->inventory->created_at,
        ]);
}
 
    