0:00 All right, so before we begin actually creating our 0:04 workflow, we're gonna create the supporting services 0:08 that 0:08 the workflow actually uses. So in this application, 0:12 again, we're going to have an ordering system 0:14 where we have an inventory 0:16 service that manages the inventory, order service to manage the 0:19 order, payments for payments. 0:23 And all of these are going to be orchestrated as part of the workflow. 0:26 When a user places an order, there are several steps that we 0:30 need to execute. For example, we need to charge the user's payments 0:34 using the payment service. We then need to reserve the stock using 0:38 the 0:38 inventory service, and finally, send the user an email to 0:42 request a review. So all of this is a workflow that 0:46 we want to handle success and failure cases, and we're 0:50 going to see how this can work across multiple instances and 0:53 potentially even services. So to begin, let's start off with 0:57 creating our payment service, which is going to take care of actually 1:01 charging the user. Now, recall we set up our payments to 1:05 charges table inside of the database, and this is where we're gonna 1:09 record our user's payments and their refunds if the payment 1:13 has to get rolled back, which will be a key benefit to using the 1:16 workflow, is that when a failure occurs, we want to refund the 1:20 user, a very common use case, and we're gonna have all of this 1:23 automatically 1:24 done with retries out of the box. And so we're recording any 1:28 payments or refunds that do occur on our database. 1:31 Now, what I wanna firstly create is the 1:34 paymentprovider.client, and this is gonna be 1:38 the actual implementation for the payments. 1:41 And then we'll have a separate payment service that then injects that 1:44 implementation and keeps it more abstract. 1:48 And so in the provider, this is where we'll have the lower level 1:50 details of 1:51 actually implementing the payment. 1:53 In our case, that's just simply persisting to the 1:55 database, and we'll see 1:57 how this all-important idempotency key comes into play on both 2:00 sides to prevent any duplicates when retries occur. 2:04 And so in here, this could be where we, for example, build an external system 2:08 like Stripe. In our case, we're gonna use the database itself and 2:12 record our payment on there. And this could work for a real-world 2:15 application 2:16 if you wanna hold onto those payments and then bill the external provider as 2:20 well. 2:20 So let's go ahead and start off by creating the new 2:23 injectable and export 2:24 class payment provider client. 2:28 All right. So the first thing that we'll do in here 2:30 is let's get access to a 2:32 logger that we'll use for debugging purposes. 2:35 We'll get the payment provider logger, and I'll add a 2:38 constructor where now I want to inject our Drizzle 2:42 ORM provider that we set up. We need to get access to the database. 2:47 To do this with NestJS Drizzle, well, it comes with a 2:50 pre-built parameter decorator, the 2:53 InjectDrizzle decorator directly from NestJS 2:57 Drizzle, and this is going to give us the same 3:01 database 3:01 instance that we provided at the root with useFactory. 3:05 So now we have access to this. This will be the private 3:09 readonly DB. And for the type, we'll now remember 3:13 we explicitly already defined this type inside of 3:16 drizzle.ts where we used the node pg database 3:20 wrapper and then point it at our underlying schema so that 3:23 our schema definitions are the source of truth for the actual 3:27 database TypeScript types, keeping our code fully type safe. 3:32 So now we can import this database from our drizzle.js 3:36 file, and we have the database instance ready 3:39 for use. 3:41 Now, our first method is going to be to create a charge. 3:44 This is how we actually bill the underlying user. 3:48 So for this, we will extract params. 3:50 This will be an object where the user passes us the amount, 3:54 the reference, a type string, 3:58 and the payment method. These all map to our database columns. 4:02 So we have the payment method coming from the order schema, 4:06 orders type file. 4:09 So with these two params, we need to create a new charge, 4:12 and then we're gonna take 4:13 in separately an idempotency key inside of 4:17 an object. So this will be 4:19 idempotency key of type string. Now, we'll see how we use this in a 4:23 second. This is to ensure we never bill the user 4:26 more than 4:26 once. So now that we have the parameters 4:30 ready, let's go ahead and actually insert into the database. 4:35 For this, I wanna destructure the response, which 4:37 is gonna be an array where we 4:38 have the existing charge if it already exists. 4:43 So for this, we await 4:44 this.db.select.from 4:48 and then pass directly in the payment charges table that we're querying from. 4:54 And then we use .where, which allows us to pass in operator 4:58 functions like the equal function. 5:01 So we can pass this in, which takes in the 5:05 underlying column on the schema that we're trying to compare against. 5:09 Here, I wanna check comparison against existing 5:13 idempotency key for any payment charge that already exists 5:17 against the idempotency key passed into us, and this 5:21 will pull back any existing payment charges with this 5:24 idempotency key. 5:26 Now, the reason why we do this is because this idempotency key is what 5:30 protects us from duplicate charges. 5:32 A key component to NestJS workflows is that it allows 5:36 for automatic retries. If any step along our 5:40 workflow fails, we will automatically retry. 5:43 And so what that means is that we could potentially have this create 5:46 charge method 5:47 called more than once. 5:50 Because of this, we need a way to deduplicate the requests 5:53 and make sure we 5:54 only bill the user one time, and that's exactly what this idempotency 5:58 key is doing. We're selecting to see if any payment charges already exist with 6:02 it, and now we'll check if existing is true. 6:05 Then we can warn a message with the logger and say, "Replayed 6:10 idempotency key. 6:12 Idempotency key for debugging. 6:17 Returning charge existing.id 6:21 not charging again." And what we're gonna do is return 6:25 directly without actually recharging the user. 6:28 For this, we're just gonna pass through all of the 6:31 existing properties from the 6:32 already existing payment charge in the database that we pulled 6:36 back. So that will include the ID, the amount, and 6:40 the underlying status from that payment charge. 6:44 If it does not already exist, then we wanna create a new charge and 6:48 persist it to the database, which will represent our charging the 6:51 user. 6:53 So for this, let's create the new payment charge 6:55 object, and this is 6:57 going to be a new object where we specify the ID. 7:01 So here, I want to use a expression with the 7:05 charge prefix and then a random 7:09 UUID beginning. So I'm gonna use 7:11 randomUUID 7:14 v7 from bun, replace to match 7:17 this format, and now we have a unique identifier for 7:21 the 7:21 charge, 7:22 which will be the primary key that we insert into the database. 7:27 The amount will come directly from the parameters that are passed into us. 7:31 And then next up for the status, we'll check to see if 7:34 params.paymentmethod equals PM_CARD_DECLINED, which 7:38 represents a card that is declined. 7:41 We persist the declined status. Otherwise, let's persist the 7:45 succeeded status. And now we can directly insert the new 7:49 charge 7:49 into the database using db.insert, 7:53 passing through the payment charges schema, and use values 7:57 to pass in a new object where we'll spread all the 8:00 existing properties on the charge, the idempotency key, 8:05 and finally, reference, which will reference the actual order that 8:09 this is associated with coming from 8:11 params.reference. 8:14 Now, we also wanna create a new type to 8:17 represent the 8:18 charge. 8:19 So let's export interface Provider Charge, 8:23 where we'll add the string 8:28 amount 8:31 and status, which will be explicitly of type 8:36 succeeded 8:37 or 8:39 declined. And the reason we're doing this 8:41 is so we can assign it to 8:43 the charge object that we create, which will make Drizzle ORM 8:47 happy about the format of the values object here. 8:51 And now we successfully charge the user by persisting it to the 8:55 database. I'll add a new log statement 8:59 where we output a message and say, "Charged charge.amount 9:04 divided by a hundred to fixed for params reference," 9:08 and include the charge ID and charge.status just for some 9:12 debugging purposes. And then finally, we'll return the charge back to the 9:15 caller. Now, at this point, we have a fully working create charge 9:19 method to charge a user inside of our workflow. 9:23 Importantly, this is also deduplicated and safe to run multiple 9:27 times thanks to our idempotency key check 9:31 because we insert the idempotency key into every payment 9:35 charge and then check against that before we actually reinsert 9:39 any new payments. 9:41 Now, next up, let's go ahead and create the new 9:45 async createRefund method. 9:48 So here, we'll take in params with type charge ID, 9:53 which will be the charge that we're refunding and a reference, 9:55 an existing charge 9:57 in the database. And just like we did for the payment, 10:00 we also want an 10:01 idempotency key for the refund. 10:04 This is because, again, our workflow can execute these steps 10:08 multiple 10:08 times, 10:09 and that's a big benefit of it. And so we need to deduplicate this in case 10:13 it ever 10:13 does succeed and it gets retried. We don't wanna refund more than once. 10:18 So what we're gonna do is create const inserted, 10:22 and this will be await 10:23 this.db.insert into 10:27 payment refunds now, the separate table, 10:29 .values. 10:32 And let's insert the object where we have the ID of 10:36 the refund now. So just like we did for the payment 10:39 charge, I want 10:40 to make this in the same format with an RE prefix 10:44 and randomUUID v7. We also have the 10:48 idempotency key, 10:50 which will deduplicate this, and the charge ID, which is what 10:54 the charge ID associated to the params.chargeID, 10:58 and that's the original charge we're refunding. 11:01 This refund implementation is actually already idempotent by 11:05 nature because the way our system will work is that if a refund 11:08 record for a given charge ID ever exists, that means it is 11:12 already refunded. 11:14 So we can use the onConflictDoNothing method here to 11:18 automatically not throw an error if a record with the idempotency key 11:22 already did exist because this is a unique 11:25 constraint that we applied on refunds. 11:28 So we're saying here that's okay. If we do insert a duplicate 11:31 idempotency key, that is fine. We will do nothing, 11:36 and we do not have to re-- and reinsert that record 11:38 or throw an error. 11:41 Instead, we will use the returning to give us the 11:44 created 11:45 record back if it was able to insert, 11:48 and we'll check if inserted.length is greater than zero. 11:52 Let's log and simply say, "Refunded charge 11:56 params.chargeID." 11:58 Fix this typo in our parameter. And now we have a refund 12:02 functionality and create charge using our Drizzle 12:05 ORM database connection, which is excellent. 12:09 This is fully deduplicated both in refunds, thanks to our 12:13 unique constraint on the idempotency key. 12:17 And our check on create charge. So now let's go ahead 12:20 and actually use this 12:21 inside of the payment service to provide the workflow with the 12:24 generic charge functionality that it needs. 12:27 So for this, let's start off by export interface 12:31 charge, 12:32 where we export the charge ID 12:34 of type string 12:37 and the amount of type number. This will be a generic charge interface 12:41 that the workflow can actually reference and what we return back from 12:45 the charge method in here. 12:47 This will be consistent across any client provider that we have, whether it's 12:51 our database, Stripe, or any other. 12:55 And then we'll add the new injectable class 12:59 payment service. 13:02 We'll also then inject our private read-only 13:06 payment provider client that we just set up of type payment 13:10 provider client. 13:13 And now we can use this directly thanks to NestJS injection. 13:18 We'll finally add the async charge method that gets passed in an order 13:22 of type order from order JS schema 13:26 and the idempotency key to deduplicate the request. 13:30 Thanks to the fact that our workflow can retry this 13:33 functionality, 13:34 and we'll return back a promise type charge now. 13:38 So now inside of charge, let's go ahead and await 13:41 this.paymentproviderclient.createcharge, 13:46 and we'll pass through the parameters that it expects, like the 13:50 amount coming from the order.total. 13:55 Reference will be the order.id 13:58 because that's what we're associating the payment with, this given order. 14:02 And payment method using order.paymentmethod. 14:07 Finally, we wanna deduplicate this, so we'll pass through the idempotency key. 14:12 And now we have the charge back 14:16 from the payment provider client. 14:18 We can check on it on the status if it was succeeded 14:22 or declined. 14:23 If it was declined, then what I wanna do is throw a new 14:27 special type of error in our system that tells our NestJS 14:31 workflow to not retry. Now, by default, 14:35 as I've been mentioning throughout this lecture, NestJS workflow is 14:39 automatically gonna retry any errors that occur during 14:43 the steps inside of the workflow, and the steps are simply these 14:47 methods that get called like charge. 14:49 So if an error does get thrown, we assume by default this 14:53 is retryable, and this is extremely useful in a distributed 14:57 environment, 14:57 right? Where we call external providers, servers are down. 15:01 We want the ability to retry. However, there are some 15:05 scenarios, like in this case, we don't wanna retry. 15:08 We wanna fail fast. Here we know the payment provider was 15:12 declined, 15:13 and we don't wanna retry charging the user because we already 15:17 tried and the idempotency key is burned and the user 15:21 was declined. 15:23 So we wanna define a special type of error 15:27 that the workflow will not retry to. 15:30 And the amazing thing is, is NestJS workflow comes with this out of 15:33 the box. 15:34 We simply just throw a new non-retryable 15:38 step error. And again, this just tells the workflow that we do 15:42 not want to retry this error that is thrown. 15:45 So here we'll say the card was declined 15:50 for 15:51 order, order.id, and now the workflow will not 15:55 retry this charge step just as we want. 15:58 Otherwise, if it did succeed, we want to return back to the workflow, 16:02 the charge ID, 16:05 and the underlying amounts coming from charge.amount. 16:08 So now we have the charge functionality working end to end thanks to the 16:12 payment provider client, and this retryable step error is handled 16:16 appropriately. The last thing we wanna do 16:19 is create the refund method. 16:21 This will get passed in the charge ID that we're refunding 16:25 and the idempotency key for the refund request. 16:28 This will return back avoid promise. 16:30 And now we'll simply just await 16:33 this.paymentproviderclient.createrefund, 16:37 pass through the charge ID 16:40 and the idempotency key. So now we have charge and 16:44 refund fully implemented, and our payment service is ready to use 16:47 inside of the workflow to charge the user, which is excellent. 16:52 Let's finally wire it in back into the root app module. 16:55 Inside of the provider's array, we wanna make sure we add the 17:00 payment provider client and the payments 17:04 service. 17:05 Now, you might see some bun type errors after 17:09 we 17:09 bring this into the application. For this, back inside of the 17:13 payment provider client, make sure you're importing 17:17 the payment method type using this import type 17:20 syntax, and the same thing for the database type. 17:25 Now our application is up and running, and it has the payment service and 17:29 the payment provider client fully ready to use inside of the workflow, 17:33 which is great. In the next lecture, let's move on to adding the 17:36 inventory service. So after we charge the user, we want 17:40 to update our internal inventory to 17:43 subtract the item from our stock so we do not oversell it. 17:47 Let's go ahead and do this next.