Motivating price history0:00
It would be useful if we tracked the history of a product's price and availability, because then you could render it on a graph. You could see, oh, six months ago this item was at an all-time low, but currently it's 50% more than that, so maybe you should wait for a sale. You can graph all of that if you collect the data. So let's give it a shot. I'm going to make a test called ProductHistoryTest. And it's not really a feature test, it's not really a unit test either. It's an integration test, but I'm going to throw it in there. And we'll find it here.
Writing integration test0:32
It's an integration test, but I'm going to throw it in there. And we'll find it here. I'm going to quickly use our current TestCase, I'm going to hit the database, and we'll get started. And we'll say it records history each time stock is tracked, something like that. So given I have stock at a retailer, if I track that stock, a new History entry should be created. Something like that. So let's go ahead and seed our database with our retailer. Where is it?
So let's go ahead and seed our database with our retailer. Where is it? All right. If I track that stock, so it's going to be the first one. So yeah, if I call track on it, as we've learned, that'll hit the API. So we're going to fake that. And then it updates our local database with the current status of the stock. So why don't we, well, first, I don't actually want to hit a database. So let's fake that. And we'll just have it return a salePrice of $99 and onlineAvailability of true.
So let's fake that. And we'll just have it return a salePrice of $99 and onlineAvailability of true. And by the way, if we keep doing this, think about it. If we need to add an additional attribute, we would have to update that everywhere that we faked the call. So at that point, you might even want to extract this to a helper method so that we only have to update it in one location. Okay. So if I track the stock, a new history entry should be created. Yeah, so to start, I'm just going to say, well, just count the number of records in
Creating History model1:58
So if I track the stock, a new History entry should be created. Yeah, so to start, I'm just going to say, well, just count the number of records in this new history table, and that should be one. But before we run this php artisan migrate, that should be zero. Okay, so I give it a run, and the History model doesn't even exist. All right. Make me a model called History and include migration. All right. Now I can import that, and we'll give it another run. Yeah.
Recording history on track2:24
Now I can import that, and we'll give it another run. Yeah. And we changed the error. So we're not creating a record in that table yet. So it returns zero. All right. Let's get to work. I'm going to do this the simplest way I can think of right now. When you track the stock, we're not going to fire an event, we're not going to hook into a model event, I'm just going to do it right down here.
When you track the stock, we're not going to fire an event, we're not going to hook into a model event, I'm just going to do it right down here. And we'll do the most basic thing I can think of, just create a new History record there. So I give it a run, and that is returning green. Because it is true, every time you track the stock, a new record gets added to the history table. But of course, it doesn't contain the information we're interested in. So if I come back, let's do this. Let's grab that first item and inspect it. So for example, the price for that history item should be the current price of the stock.
Let's grab that first item and inspect it. So for example, the price for that history item should be the current price of the stock. So I could say the stock price should match the history price. Give that a run, and it fails. Oh, that's wrong too. I guess that's in dollars. Yeah, we fixed that, but still actual is set to null. So yeah, what we're going to do is visit our migration, and we now know we are storing or recording the current price. And if we give that a run, now the model is going to fail because we didn't provide a
Adding required migration fields4:07
I want to keep track of the price if it's currently available. So that would be this, and we'll do all of them for brevity. I need to track the product, so that would be historyProductID. And then also the stockID needs to be tracked as well. I think that's right. So I'm going to give that a run, and it fails. So we'll make these pass as quickly as we can. So we want the foreign ID for the productID, and that's constrained, and then cascade on delete. Okay.
on delete. Okay. Next, I want the stockId, then the price, and then the availability, and that would be a Boolean. All right, so we give it a run, and it fails. Oh, no such table main.stocks. What? Oh, I know what it is. The table name for our Stock model is actually stock. It's not stocks.
The table name for our Stock model is actually stock. It's not stocks. So we have to be specific, because again, Laravel is looking for the convention. So if you need to overwrite that, you can pass the table name here. At least I think that's it. Run it. Yeah. Okay, so now we've changed the error. Now we're not setting those new fields that we require, or those attributes. So we'll come back, and we'll set the ... I'm going to do this manually to start.
Now we're not setting those new fields that we require, or those attributes. So we'll come back, and we'll set the ... I'm going to do this manually to start. The productId is whatever the productId is for the stock. The stockId, and then the endStockStatus will again match up. All right, fingers crossed. And that returns green. Okay, so now we know every time a stock is updated, and maybe that's every five minutes. At that point, we record the current price and availability, and that's going to change. Maybe at 10 o'clock, it's out of stock, but at 10.05, it's in stock. At 10.30, the price is $100, but the next day at 9 a.m., it's $95.
Maybe at 10 o'clock, it's out of stock, but at 10.05, it's in stock. At 10.30, the price is $100, but the next day at 9 a.m., it's $95. All of that is going to be recorded in this table here. Okay, so now it's passing. Let's go back to StockTest. Just make sure the whole class is passing. So now we can tweak it if we need to. For example, it makes sense for a relationship to exist. A Stock has History. So we could start there, and think about it.
Defining stock-history relationship6:32
A stock has history. So we could start there, and think about it. If the history table has a reference ... Actually, hold on. The table name is called histories. We'll take care of that in just a moment. That doesn't make sense. That relationship would be hasMany, right? A stock can have many records worth of history. So that would be history. Then you could say, this history, create a new entry.
So that would be history. Then you could say, this history, create a new entry. And now if we do that, the stockId will be set automatically, so I should be able to delete that. And with a little luck, it still passes. We might even want to extract this to a method, just to make it a little more clear, like recordHistory. Run it, and that passes. Or maybe, I don't know, should the product itself be the one recording the history, rather than the stock?
Or maybe, I don't know, should the product itself be the one recording the history, rather than the stock? Maybe? I'm not sure. Maybe that would be better. We can think about that. Let's bring this up, by the way. We can think about that and change it, but the important thing is it's working. So we give this a run, and there we go. So why don't we php artisan migrate my local database and try this out?
Renaming table and model7:40
So we give this a run, and there we go. So why don't we migrate my local database and try this out? If I come to TablePlus, we give it a refresh, and we now, oh, dang, I forgot to fix that. Okay, tell you what. Come along for the ride. We're going to do this as quick as we can. php artisan migrate:rollback. So just undo that new table. Go to our migration. I'm going to call this now, create_product_history_table.
Go to our migration. I'm going to call this now, create_product_history_table. But notice, as we do that, phpStorm tries to do its best to update the file name, and that's fine. This will now be product_history, and then if I go to the History model, I'm not going to use the convention name for that model. It'll be ProductHistory. Okay, did that work? Give it a run. Ta-da.
Running command and verifying8:29
Give it a run. Ta-da. All right, 30 seconds flat. So now, if we run it again, and I go back to TablePlus, give it a refresh, there we go. Okay, so now, every time we track the stock for a product, as part of that, it should create history. Let's try. php artisan track. All right, give it a run.
phpArtisanTrack. All right, give it a run. Now if I refresh, at this exact timestamp, the price was $299, and it was out of stock. So let's fudge this a little bit. Let's say it was $329 and in stock at that point. But five minutes go by, the command automatically reruns, and now we can see, all right, well, the price dropped by $30, but it's no longer in stock. So now, if you're collecting this information every five minutes, pretty quickly you have six months worth of history that you can then chart to help out the user.