0:00 All right. So next up, we're gonna take a look at creating the 0:03 inventory 0:04 service, which is going to manage reserving the 0:07 stock inside of our workflow. In our workflow, after a 0:11 user's card is charged, then we want to reserve the 0:15 stock and make sure we actually have the item available to 0:19 sell to the user, and of course, make sure that we 0:22 update the stock so that we don't oversell the product. 0:26 So for this, inside of our source directory, 0:28 let's continue by creating a 0:30 new inventory directory, 0:33 and we'll create our 0:36 inventory.service.ts. 0:39 Now, this is gonna be a straightforward NestJS 0:42 injectable class without 0:43 any extra dependencies. 0:46 So we'll export class InventoryService, 0:50 and let's add a private readonly logger, 0:53 use new Logger, and I'll give this a prefix of 0:57 warehouse to represent our warehouse. 1:00 Now, in the constructor, let's use again our injectDrizzle 1:04 decorator 1:04 from NestJS Drizzle and inject the private readonly 1:08 database of type db... Database from our database schema. 1:13 So now we have the database. We want to implement two methods. 1:17 So just like in our payment service, we have a way to charge 1:21 the user and then a way to undo the charge, right? 1:24 This is known as a compensation inside of our 1:28 workflow or a distributed 1:30 transaction/saga. We have 1:34 a step and a compensating step which undoes that. 1:38 Now, again, we know why we wanna undo, right? 1:40 If some step inside of the workflow fails, and as we'll see later 1:44 on, this can be due to a actual failure in the 1:47 workflow or a real-world failure, right? 1:50 Maybe, for example, as we'll see, 1:53 the order failed to get delivered to the user. 1:56 If that's the case, we need to undo the steps of the workflow 1:59 that we've 2:00 already done, and that's what the workflow allows us to 2:03 do by 2:03 refunding the payment. And just like we refunded the payment in 2:07 inventory service, we need a way to reserve an inventory 2:11 and release the inventory. This is a key component that I want you to 2:15 understand. Inside of every workflow step, we have 2:19 a step that actually does the work we want, and then we need a 2:23 corresponding compensation action which will undo that. 2:27 We'll see how this compensating step ties into the workflow 2:33 later on when we create it, but this is a key benefit because it allows us 2:37 to 2:38 keep our application in a reliable, consistent state. 2:41 If we need to roll back our business workflows, we can do 2:45 that. 2:48 So enough talk. Let's go ahead and create the reserve 2:51 method. This will take in the underlying order 2:55 items that we are trying to reserve. 2:58 And just like we had an idempotency key for the payment, we're also 3:02 gonna have an idempotency key for the reserve request. 3:05 Because remember, every single step inside of the workflow 3:09 can be 3:09 retried, and we wanna make sure that we do not 3:13 duplicate this reservation step 3:16 and it only happens once for a given step in our workflow. 3:20 So we're gonna deduplicate this with the idempotency key once again. 3:25 This will return us back a promise where we have the 3:29 reservation ID of type string. 3:34 And now let's return 3:35 this.db.transaction. 3:39 We're going to wrap this inside of a database transaction, and we'll see why 3:43 in a second, because we're gonna have multiple steps to 3:46 this, and we want this 3:47 to be atomic. All of these steps in the database should 3:51 fail or 3:51 succeed together. 3:53 So inside of here, we'll get access to the async transaction, 3:57 which will 3:57 allow us to keep everything inside of the transaction so they all 4:01 succeed or fail together. Now, just like we did for the payment, the 4:05 first step that we want to s- check to see is if we already 4:09 have a reservation with this given idempotency key. 4:13 If this step did already succeed, well, we simply just wanna return it and not 4:17 do any additional work. 4:20 So let's go ahead and check this first. 4:23 We'll destructure existing and call await 4:27 transaction.select.fromReservationsSchema 4:34 and use .where and look for the equality operator 4:38 and pass in the 4:40 reservations.id. And this is how we're 4:44 gonna match the idempotency, because our reservations 4:48 doesn't have an idempotency key. But remember, as we specified in the 4:52 schema, we're gonna treat the reservation ID here 4:56 as the 4:56 idempotency key because it's a primary key. 5:00 It's unique. We can't have multiple of them, 5:02 so we can treat this 5:04 as the idempotency key, and that's exactly what we'll do here. 5:07 We'll try to look for a reservation ID that 5:11 has an idempotency key that's being passed in. 5:15 If we did find an existing reservation, we'll 5:19 simply return back 5:22 the object where we have the reservation ID set to 5:26 existing.id, and this returns back the 5:29 existing reservation. This is in case we retry this step and it 5:33 already had succeeded. We have successfully handled this case. 5:37 So next up, what we wanna do is iterate over 5:41 all of the order items that have been passed into us. 5:45 This reserve method is going to allow us to reserve multiple 5:48 order items that are a part of a single order. 5:52 Because remember, this order item interface that we've 5:55 defined and are using here, 5:58 this maps to 6:00 our 6:02 Order schema, we have our items array that we're storing as a 6:06 JSON blob. So a single order will have multiple items 6:09 inside of it. And also remember, these items map 6:14 to an underlying stock table. 6:16 So 6:18 an order item will have a product ID associated with it, and it will have 6:22 a quantity associated with it. And it's our job in this workflow to 6:26 then verify that first, we actually have a 6:29 quantity, a valid quantity to allow the user to 6:33 buy. And then, of course, we then need to decrement the quantity to 6:37 reflect 6:37 the newly purchased order item. And so let's do this. 6:41 We're gonna iterate through each of these order items, and 6:45 for each one, we're gonna have const updated await 6:49 transaction.update. 6:52 And here, we're now updating that stock table that we just looked at. 6:57 We wanna call .set. And here, as I mentioned, 7:01 we were updating the quantity. 7:03 So to update the quantity, we can use a raw SQL 7:07 statement inside of a template literal 7:10 and s-- use stock.quantity, point at the actual 7:14 column we are 7:15 working with, and then we can use the subtraction 7:19 operator and subtract directly the 7:22 item.quantity that we're iterating over. 7:25 And so here, this SQL statement will automatically 7:28 subtract the 7:29 item quantity from the current stock.quantity. 7:33 And this is an atomic operation 7:36 thanks to this SQL operator function. 7:38 Then we're gonna use .where to look at the 7:42 correct stock to update. 7:45 Here, we're gonna use the and function 7:48 and pass in two expressions. The first is equals. 7:52 This one's simple. We want to look at the 7:54 stock.product ID 7:57 that matches the item.product ID. 8:01 So both of these have to match. And then for the second operator 8:04 here inside of this and compound expression, 8:08 we're gonna pass in greater than or equal to. 8:12 And here we're gonna pass in stock.quantity 8:15 and then item.quantity. So what this is doing is it's 8:19 checking that the stock entry that we're updating 8:23 is, of course, the correct product ID, but it's also checking here, 8:27 thanks to greater than or equal to, that the stock quantity has 8:31 to be greater than or equal to the item.quantity that 8:34 is being 8:36 actually passed in. Now, of course, this 8:39 is going to ensure 8:41 that the stock quantity actually exists and is 8:45 available, right? If the item.quantity is 8:49 greater than the stock.quantity, well, that doesn't make sense. 8:52 We can't reserve more than the stock actually has, and that's 8:56 exactly what greater than or equal to lets us do here. 8:59 As we can see in the description, it tests that the first expression 9:03 passed is greater than or equal to the second expression, 9:06 and that's what 9:07 we want. The stock quantity has to be greater than 9:10 the item quantity 9:11 that we're trying to reserve here. 9:14 And then finally, let's pass .returning to get 9:17 the return value. Now, the reason we want the return value here 9:21 is 9:21 because now we want to check if 9:25 updated.length, the return value, is equal to zero. 9:29 If this is equal to zero, what this means is that, well, this 9:33 expression did not get matched because the 9:36 stock.quantity was less than the item quantity, and 9:40 this is invalid. This means we are out of stock for this given item. 9:45 Now, at this point, we want to fail the step and 9:49 throw back to the workflow, and we don't wanna retry this because, of 9:53 course, we know this is not a retriable error. 9:55 If we're out of stock, retrying is not going to help us. 9:59 So we're gonna use the 10:01 new non-retriable step error that we used before 10:06 and pass in a template literal that says 10:08 item.product ID is out of 10:12 stock. And critically, we know that since we're throwing an error 10:16 here, 10:18 well, even if this is, for example, the fourth item in the array that we're 10:22 iterating over and we just ran out of stock for that item, 10:26 well, since we're in a database transaction, 10:28 throwing an error here 10:30 is going to undo all of those other updates and 10:33 make sure we're not left in an inconsistent state. 10:36 And so this is the critical part of the database transaction that we're wrapping 10:40 all of this in. We need to make sure that every order item 10:44 inside of the order has available stock, and the database 10:47 transaction allows us to do this. So at this point, after we've 10:51 updated every single order item stock quantity, we can 10:55 now successfully insert a new reservation into our 10:58 reservations table because we have updated all of the 11:02 system stock quantities. So we're going to await 11:06 transaction.insert into the reservations table 11:10 as we're creating a new reservation, and we'll pass through the item potency 11:14 key as the ID 11:16 and the order items here. Remember, the reservations table 11:20 here, we were also treating the ID here as the 11:23 primary key, 11:25 and we're storing the raw items as JSON blobs. 11:29 And so now we have the reservation stored, which is great. 11:33 This means that the user's order is actually saved. 11:36 We've updated our quantities, and we have all of the items 11:39 that we're storing in 11:40 the reservation saved. 11:42 I'll also add a log statement 11:46 where I say 11:48 reserved items.map and output a simple expression with 11:52 the item quantity and the item product ID. 11:56 Finally, make sure we return back the return object 11:59 with reservation 12:00 ID set equal to the item potency key. 12:03 And now at this point, we have successfully 12:06 reserved the order. 12:09 By updating all of our stock quantities 12:14 and then inserting into the reservations table the user's reservation. 12:18 So this is excellent. We now have the ability to reserve an 12:21 order. 12:22 Now, as I said before, we need the opposite, a way to release a 12:25 reservation if for whatever reason it fails, or as we'll see later 12:29 on in the workflow, a order could time out or 12:33 the order carrier could fail to 12:36 deliver the order. If that happens, we know we need to 12:39 release the order from our reservations table and update our stock 12:43 quantities in the opposite way. So let's do this next. 12:47 All right, so let's create the async release method. 12:51 Now, this method only needs to take in the 12:53 reservation ID 12:55 because this already exists in the database, 12:58 and we'll return promise void. 13:01 So here let's wrap again everything inside of a single database 13:05 transaction so it's fully atomic and everything succeeds 13:09 or fails together. And now what we're gonna do is 13:12 destructure the reservation from the database and call await 13:16 transaction.update 13:19 on the reservations table. And I now want to use 13:22 .set to set a property on the reservation. 13:26 Here we're going to set releasedAt new Date. 13:30 So remember, on the reservations schema, we have the special property 13:33 releasedAt which tells us if a reservation was rolled 13:37 back. 13:39 And that's exactly what we're doing by setting releasedAt to a new date. 13:43 Then let's go ahead and use .where. 13:46 This is gonna take in another compound and where we set 13:50 equals 13:52 StockReservations.id is 13:56 what we're comparing, and we're looking at the 13:59 reservation ID that gets passed in. So that's straightforward. 14:02 We wanna release the reservation that we're asking for. 14:06 But then we also wanna check that the reservation that we're releasing 14:10 isn't already released. For this, we can use 14:14 isNull function that will check a 14:18 given column, in this case reservations.releasedAt 14:22 is null. So what this says is we're looking for the reservation with 14:26 this ID, and it doesn't already have releasedAt 14:29 set. 14:30 So if this is the case, then we know 14:34 we need to release the reservation. 14:36 However, 14:37 if we did not find a reservation because the 14:41 ID didn't exist or it already had releasedAt set, 14:46 then we're simply gonna return early. 14:48 We do not wanna carry on because the next step is to 14:52 update our stock quantities, and we do not wanna duplicate that ever. 14:56 We only want to release a reservation once and keep the stock 15:00 quantities fully correctly in line. 15:03 And don't forget to use .returning at the end to get the 15:07 value back. And so now at this point, what we're gonna do 15:11 is iterate over const item of 15:14 reservation.items and iterate over each and every 15:18 one and update the stock quantity. 15:21 So here we'll just await transaction.update on stock, 15:25 call .set, 15:27 and now do the opposite that we did earlier. 15:29 So now we're setting quantity. 15:31 Well, let's use the same SQL template literal, 15:35 and 15:35 again, we're just targeting stock.quantity. 15:38 But now we're adding the item.quantity and not subtracting it 15:42 because we're undoing what we did in the reserve where 15:46 we subtracted the item quantity. Now we're adding it back 15:50 because remember this is getting rolled back in the case 15:53 that the order 15:54 failed to get delivered to the user or it timed out. 15:58 So we're doing the opposite. We're updating the stock to set it back to 16:01 where it 16:01 was before. 16:03 So we're adding the item quantity back onto the stock.quantity here 16:07 correctly, and finally add a where clause where we 16:10 simply pass in the equal function, 16:14 stock.productID is equal to the 16:16 item.productID. 16:19 And now stock quantity is fully back to where it was before, 16:23 rolled back correctly. And as a last step, we can log a statement 16:27 and say, "Released reservation," and pass in 16:30 reservation ID. So now at this point, we have the ability 16:34 to not only reserve a new order in our system, which will 16:38 appropriately update the stock quantity, but we can also roll that 16:42 back and reset the stock quantity at any given point. 16:46 And as we'll see later, this is a huge benefit to this workflow 16:50 approach because when anything goes wrong at any point in 16:54 time, we have the ability to roll back, and this is known as a 16:57 compensation. We're gonna see how this is used as we implement the 17:01 workflow next. So finally, don't forget to go back to the root app 17:05 module, and let's wire in this new inventory 17:08 service into 17:09 our providers array. And also don't forget to update the import 17:12 here for importing the database type to explicitly 17:16 declare type. 17:18 Now you can see the application is still running successfully. 17:21 We have our new service wired in, and at this point we 17:25 are finally ready to start actually building out our workflow 17:29 with our new payments and inventory service. 17:32 I'm super excited to get started with this in the next lecture, where 17:36 we're gonna use these services inside of a brand-new workflow and see 17:40 how we can implement this complex business logic, handle failures, 17:44 retries, and even rolling back at any given point 17:47 inside of the workflow. I'm so excited to jump in 17:51 and see how we can do 17:51 this with NestJS Workflows. Let's go ahead and jump in 17:55 in the next lecture.