Build a Booking System With React, GraphQL, and Stripe API
Before we begin to implement the `createBooking` mutation resolver, we'll first create the utility function that will facilitate a Stripe charge with the help of Stripe's API.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two with a single-time purchase.
[00:00 - 01:22] Before we begin to create the Create Booking mutation, let's first create the utility function that will facilitate a Stripe Charge. We'll need to have this capability to allow the person who's making the booking to pay the owner of the listing a set price before we can say the booking is finalized. The good news is we have a Stripe instance established in our libapi folder where we've already instantiated a Stripe client and a connect function where users able to connect their Stripe account with our Stripe tiny house platform. We'll create another asynchronous function within this instance called Charge. Let's take a quick look at the Stripe Connect API documentation to gather what we'll need to do to create a Stripe Charge. We 'll be sharing the link to this documentation in the lesson manuscript. The term Charge constitutes having a user or someone pay something. There's many different types of charges that can be created but we'll create a simple direct charge which is recommended for standard accounts which are the account types that are connected with our Stripe Connect platform. In this context the connected account, not our account, will be the ones responsible for Stripe fees, refunds and chargebacks.
[01:23 - 03:15] We're looking at the Node.js example here for the charges API and essentially with the Stripe client available we can create a direct charge with the create function available from the charges object within the Stripe client. The first argument of this function is a data object that is to contain information about the Stripe charge. There's many different fields we can specify here with some being required. The first required value is the amount which is the amount the connected user is going to be receiving or in other words the amount that we're going to charge the person making the payment. The second data value will provide is the currency which is another required data option. This is to be a three-letter ISO code to represent the currency. For our application we'll be charging everyone with US dollars. Note that the amount refers to the smallest unit of the currency so with the US dollars the amount will be incensed. The third data value is the source which is an optional data option but it's useful since it represents the source being charged like the ID of the credit card, debit card, bank information etc. In our case the value for this source will be passed in from the client. The last data value will provide though optional will be very important for us and that is the application fee amount which is a fee and cents that is to be applied as a charge and collected as the Stripe account owner which is us. Now as to how we're going to determine which Stripe account connected account is going to receive the payment that will be the value we introduced in the second header options object for the field of Stripe accounts.
[03:16 - 05:46] This is where we'll provide the Stripe user ID we've collected when the user connects with their Stripe account and we've stored as the wallet ID field of the user document. Note that this is how we essentially have the person making the payment pay the owner of the listing. Remember the person who's making the payment doesn't need to have a Stripe account. They just need to provide their payment information and in our application that will be their credit card or debit card details. Once the charge is made Stripe will charge their payment information and here by applying or providing a Stripe account ID we're essentially taking that charge and saying pay out to this Stripe account. We're the middleman. We simply are going to receive the amount or the fee for using the platform. We're going to set up our charge will be very similar to what's being shown here. So let's go back to our Stripe file and set it up. This charge function we have would accept a few arguments from the mutation resolver that will eventually establish. It would accept an amount of type number. It would accept a source of type string which would further come from our React client app and it would accept a Stripe account of type string which is the value from the wallet ID field from the connected user document. We'll call the create function within the charges class of our Stripe client. For the data object we'll pass the amount along. For the currency we'll say USD since you want to charge everyone in US dollars. We'll also pass the source from the argument along as well. The application fee amount is the amount we want to charge the user to use our application tiny house. We can provide any value here up to the amount we 're going to make the charge but on the client we're going to tell them that we're going to charge an extra 5% of whatever they're about to pay the owner of the listing. And though not very necessary we'll say it'll be approximately 5% and we'll use the math.round function to round the amount to the nearest integer. And in the options header object we'll specify the Stripe account and pass the Stripe account value that will be passed into this function as an argument.
[05:47 - 06:20] From the response we'll be able to look at the status field. This status of the payment will have one of three values, succeeded pending or failed. Since we're going to await for the completion of the asynchronous charge function the status at this point should either be succeeded or failed. We'll do a check and say if the status of this charge response is not succeeded we'll throw an error that says failed to create charge with Stripe.
[06:21 - 06:42] And we'll disable the camel case ESLint rule around our Stripe client charge function. With our Stripe charge capability established let's begin to work on the Create Booking mutation.
[06:43 - 07:21] We'll first need to update the GraphQL type definitions for Create Booking. Just like our other mutations we'll have this one expect an input of an object that will create shortly called Create Booking Input. When this mutation gets resolved successfully though we may not necessarily need this resolved information in the client we 'll follow the pattern we have and have the mutation return the primary entity that's either been mutated or created which in this case will be a booking object itself. We'll create the Create Booking Input object type.
[07:22 - 08:47] There's four required fields we'll want the client to pass in the input when the Create Booking mutation is triggered. We'll want to have the ID of the listing that is being booked which will be of type GraphQL ID. We'll want a source which is what the Stripe React component in our client will pass for us and will provide details of the payment source being charged like the ID of a credit or debit card. This will eventually get passed to our Stripe charge function we've just set up. We'll also want the check-in and check-out dates of the booking that's being made. When the dates get passed from our client to the server we'll simply have them passed in as strings that are formatted as states or look like dates. Notice that we're not passing in the amount that's going to be charged to the user. The reason being is we'll determine this amount from the check-in and check-out date values by recognizing which listings being booked from the ID being passed in will gather the details of that listing such as the price per day. We'll simply multiply the number of days the user is booking with that price. So if check-in is today and check out is two days from now including today that will be three days. If the listing price is $100 that total amount will be $300 and it will create that amount in our resolver function.
[08:48 - 09:12] Let's create TypeScript types for the input argument for this create booking mutation. We have a simple booking resolver map we've set up before so we'll create a types file in the folder here to reference the types that pertain to this particular map. We'll export an interface called create booking args that will have an input field of type create booking input.
[09:13 - 09:34] And we'll create the create booking input input interface that is to have the ID, source check-in and check-out fields. All of being type string.
[09:35 - 10:01] The ID field in our GraphQL API is of type GraphQL ID but it gets serialized as a string in our TypeScript code. In our booking resolver's map file we'll import the create booking args interface from the adjacent types file. And in our resolver function we'll specify the input argument that is expected to be passed in.
[10:02 - 10:22] We'll also go ahead and say when the resolver function is resolved it should return a promise of the booking object type. We'll also need access to the database and request objects from context so we 'll destruct them as well.
[10:23 - 10:50] We'll import the request interface from the express library as well. We'll set up a try catch statement and in the try statement we'll destruct all the fields from the input object ID source check-in and check-outs.
[10:51 - 11:21] There's going to be a decent number of things we're going to be doing just in this mutation function alone. So before we build out the implementation let's spend a little time talking about our game plan and jotting down what we want to do in comments. The first thing we'll do similar to our other mutations is we'll guard and specify that only a logged in user or a viewer should be making the request.
[11:22 - 11:48] Next when we verify that the viewer has made the request we'll try and find the listing document in our database for where the viewer wants to make the booking for. We'll specify an extra check here to say that if for some reason or another the viewer tries to book their own listing we'll throw an error since we don't want users booking their own listings.
[11:49 - 12:13] Next we'll do another safeguard. In the client we've prevented the user or viewer from placing a booking where the check-in date is before the check-out dates. This would be a pretty bad bug if ever to happen so we'll also place a server side check and say if the check-out date is before the check-in date we'll throw an error.
[12:14 - 13:04] If the dates are valid we'll look to create a new bookings index for the listing document to essentially update it with new dates. Remember how you mentioned before that we're going to use a booking index object that's going to exist within every listing document that essentially is going to determine which dates the listing has been booked in. We'll need to update the listing document for where the booking is made in which its booking index will now need to know that check-in and check-out dates for this booking. This is where we'll also check to see if the viewer has somehow tried to book dates that overlap existing already booked dates and if so we'll throw an error. We'll then get the total price we'll charge the person making the booking.
[13:05 - 14:07] We'll then look to gather the user document of who the host of the listing is. With the total price and the host information available we'll then be able to create our stripe charge on behalf of the host. And finally with our actions now complete we can look to update the various different documents in our database. We'll first need to insert a new booking in our booking's collection of the booking that's just been made. We'll then update the user document for the host to increment their income with the amount that was just paid to them. Then we'll need to gather the booking document for this newly created booking and update the booking's field of the tenant, the person making the booking. We'll also need to update the listing document that's been booked by introducing the newly created booking in its booking field.
[14:08 - 14:23] Remember, both the user document and the listing document in our collections has a booking's field to determine which bookings have been made for either this user or for this listing. And we'll then return the newly inserted booking.
[14:24 - 14:38] As you can see there's a decent amount of work we're going to be doing just for this mutation. At least now we have a bit of a game plan and we'll just fill in the work we want to do.
[14:39 - 15:24] So the very first thing we want to do is we want to verify a logged in user or in other words, a viewer is making the request. We have an authorized function established for this, which checks to see if a user can be found based on the request details. So we 'll import this authorized function from the libutels file. We'll run the authorized function, pass the database and request objects along, and check if the viewer from this function can't be found. We'll throw an error that says viewer cannot be found.
[15:25 - 16:00] Then we're going to see if we can find the listing document of the listing that 's going to be booked. We'll use Mongo's find one method to find the listing where the underscore ID field is the same as that from the ID field from the input argument. We'll wrap the ID from the input with the node Mongo driver new object ID class to have it in the format of a Mongo document object ID.
[16:01 - 16:17] And we'll import the object ID constructor from the MongoDB driver. We'll then check to see if this listing can be found. If not, we'll throw an error that says listing can't be found.
[16:18 - 17:01] We'll then handle the check to see if the listing host is the same as the ID of the viewer. So in other words, is the viewer booking a listing of their own. If this has to happen, we'll throw an error that says viewer can't book own listing. Now we'll look to see the dates the viewer has booked and see if we can check if the viewer has booked a check in date that is after checkout or if checkout is before checking.
[17:02 - 17:35] To help make this simple date comparison, we won't need to install and use a robust date library like moment. Instead, we'll just use the date constructor function available in JavaScript to convert the check in and check out string values to their date representation. We'll then say if checkout date is before or less than the check in date, we'll throw an error that says checkout date can't be before check in date.
[17:36 - 20:17] At this point, we'll look to obtain a new bookings index object of the listing document with the new dates that the user or viewer has picked. This will take a few lines of code to achieve, so we'll try and consolidate this functionality to a function we'll call resolve bookings index, which will receive the bookings index of the listing document, the check in date, and the check out date in string values. We'll create this function in a second . For now, let's look to populate the rest of this mutation function. Now with our bookings index to be prepared, we'll look to gather the total price of what the tenant will pay. This should be fairly straightforward. Here, we'll simply want to multiply the price of a listing document, which is for per day, with the number of days a booking is made. We don't have a library like moment to help us and though we could import it, we'll avoid doing so since we don't have a lot of work that deals with the date objects on our server projects. We'll do our own somewhat rough implementation that should work for the vast majority of use cases. So we know that the total price will be equal to the listing price multiplied by the difference between the check out and check in dates. To determine the difference, we'll use the get time function on the date objects, which gives us a time of milliseconds since the first of January 1980, which is the Unix epoch, and is an actual numeric value. We can subtract the millisecond time of check out with check in to get us the difference between the two times in milliseconds. With this difference available to us, we can divide it with the number of milliseconds in a day, which is 86,400,000 to get the difference in days. Now, if we checked in today and checked out tomorrow, the difference here will be one. However, we always want to count the check in days as part of the total price. So we'll add it by one. And this should give us the total price for the booking made between the check in and check out times. For a few of the next couple of things we'll do, we'll need access to the user document of the host, the person who owns the listing.
[20:18 - 21:03] So we can use the find one method on the user's collection to find the document where the ID is equal to the host field in our listing document. We can then have a check for two things. If this host object doesn't exist, we'll throw an error. Additionally, we'll want this host connected with Stripe, that's the only way we'll be able to pay them out. This Stripe, check is part of the wallet ID field of this host object. So if either the host doesn't exist or the wallet ID field doesn't exist, we'll throw an error that says the host either can to be found or is not connected with Stripe.
[21:04 - 21:57] And now if all this information is available to us, we can create the Stripe charge. We'll import the Stripe instance object we've created in our lib API folder. And we'll run the charge function within our Stripe instance and pass in the payloads the function is to expect the total price we want to charge the source, which will come from the client and passed in as an argument to this resolver. And the Stripe user ID of the person that is to be paid out, which is the wallet ID of the host. At this moment, the charge would have been made successfully, and we'll simply need to now update the documents in the different collections.
[21:58 - 24:10] We have a bookings collection to keep track of all the bookings made in our app . We'll use the insert one function for Mongo to insert a new document to this collection. We 'll add the values that will watch a new object ID for the underscore ID field. The underscore ID field for the listing to reference the listing that's being booked. The underscore ID field for the viewer for the tenant to reference who's making the booking. And the check in and check out dates in normal string format. We'll need access to this newly inserted booking document. So similar to what we've done before when using the insert one function, we'll access the first item of the dot ops property within the inserted results to get the newly inserted documents. Next, we'll look to update the user document of the host and look to increment the income field by the total price. So we'll use the update one function for Mongo and update the user document where the underscore ID field is that of the host underscore ID. And we'll use the increment operator to increment the income by whatever the total price that's going to be paid out to this host is. Next, we'll update the user document of the viewer to specify that this booking now would be in the bookings field of this viewer or user document. So we'll use the updates one method again in the user's collection, find the user document where the underscore ID field is that of the viewer, and we'll use the push operator to push a new entry to the bookings array field and add the newly inserted booking ID. Finally, we're going to update the listing document that's being booked.
[24:11 - 24:50] We'll use the update one method and find the listing document where it's underscore ID field is that of the listing we've just found from the ID passed in as the arguments. We could probably just use the ID passed in as an argument, but instead we'll be specific and say we want the underscore ID field of the found listing. We'll update two things in this document. We'll need to update the bookings index of this document with the new bookings index we've prepared. So here we'll use the sets operator to update the entire value of this field with the new value we've prepared.
[24:51 - 25:18] And similar to the above, we'll want to update the bookings array in this document to have the ID of the newly created booking. We'll use the push operator and push the ID of the inserted booking to the bookings field. And that's all the changes we want done in this mutation resolver function. Now we'll simply have the function return the inserted booking documents.
[25:19 - 25:55] And in our catch statements, we'll catch whatever error might have been thrown and place it within a new error statement that says failed to create a booking. Oof, it's quite a lot, but we're not done yet. We still need to build out the functionality for the resolve of bookings index function that will create a new bookings index based on the check-in and check-out dates. We'll look to handle this in the next lesson.
[25:56 - 25:59] [silence]