- POST: Cart Insert
- POST: Cart Update
- DELETE: Cart Delete
- GET: Cart Retrieve
- POST: Cart Clear
- GET: Order Payment
- POST: Order Complete
- GET: Order List
- GET: Order Detail
Before we can start on this backend we will need a developer account from Stripe, and get our api keys:
- Go to Stripe Developer Docs and create an account.
- Click on the
Dashboard ->button in the top right hand corner to access our Dashboard. - Click on the
Developersbutton in the top right hand corner to access out developer info. - Click on the
API keystab on the left menu. - Copy the
Secret key(you will need to pressRevel test keyto see it) and paste it as yourSTRIPE_API_KEYenviroment variable in your configuration settings. (Just like how we did with our database credentials)
We will need the public key later for our front end so refrence this guide when you want to return to that webpage. We just want to use the test data and test keys. You may close your account after the course if you would like.
Our application depends on a lot of files and resources to be able to run correctly. These files have been provided for you and are listed here for your reference. These files should NEVER be modified and must be left AS IS.
Maven gets all its settings from a file called pom.xml. This file determines the dependencies we will use in our project as well as the plugins we use for compiling, testing, building, ect..
Spring Boot has a large number of settings that can be set with a file called application.yml. We have already created this file for you and have filled it with some settings. There is a file for the main application as well as one for the tests.
There are two folders in this project that contain resources, and application settings, as well as files required for the tests.
There is a Single class that contain all of our test cases:
| 🗄 idm | 🗄 movies | 🗄 billing |
|---|
| 💾 billing.cart | ||
|---|---|---|
| Column Name | Type | Attributes |
| user_id | INT |
NOT NULL |
| movie_id | INT |
NOT NULL |
| quantity | INT |
NOT NULL |
| Constraints | ||
PRIMARY KEY (user_id, movie_id) |
||
FOREIGN KEY (user_id) REFERENCES idm.user (id) ON UPDATE CASCADE ON DELETE CASCADE |
||
FOREIGN KEY (movie_id) REFERENCES movies.movie (id) ON UPDATE CASCADE ON DELETE CASCADE |
||
| 💾 billing.sale | ||
|---|---|---|
| Column Name | Type | Attributes |
| id | INT |
NOT NULL PRIMARY KEY AUTO_INCREMENT |
| user_id | INT |
NOT NULL |
| total | DECMIAL(19,4) |
NOT NULL |
| order_date | TIMESTAMP |
NOT NULL |
| Constraints | ||
FOREIGN KEY (user_id) REFERENCES idm.user (id) ON UPDATE CASCADE ON DELETE CASCADE |
||
| 💾 billing.sale_item | ||
|---|---|---|
| Column Name | Type | Attributes |
| sale_id | INT |
NOT NULL |
| movie_id | INT |
NOT NULL |
| quantity | INT |
NOT NULL |
| Constraints | ||
PRIMARY KEY (sale_id, movie_id) |
||
FOREIGN KEY (sale_id) REFERENCES billing.sale (id) ON UPDATE CASCADE ON DELETE CASCADE |
||
FOREIGN KEY (movie_id) REFERENCES movies.movie (id) ON UPDATE CASCADE ON DELETE CASCADE |
||
| 💾 billing.movie_price | ||
|---|---|---|
| Column Name | Type | Attributes |
| movie_id | INT |
NOT NULL PRIMARY KEY |
| unit_price | DECIMAL(19,4) |
NOT NULL |
| premium_discount | INT |
NOT NULL |
| Constraints | ||
CHECK (premium_discount BETWEEN 0 AND 25) |
||
FOREIGN KEY (movie_id) REFERENCES movies.movie (id) ON UPDATE CASCADE ON DELETE CASCADE |
||
All the data to initialize your database is found in the db folder here: db folder.
All ❗ 400: Bad Request Results must be checked first, and returned before any other action is made.
The order of the checks within ❗ 400: Bad Request is not tested as each Result is tested individually.
In the case of non-successful results, where values are expected, the values should not be included, for example.
{
"result": {
"code": 32,
"message": "Data contains invalid integers"
},
"value": null
}the value key should not be included:
{
"result": {
"code": 32,
"message": "Data contains invalid integers"
}
}This is done by insuring that all null values are dropped by either:
- Having your Model extend
ResponseModel<Model>, or - Putting the
@JsonInclude(JsonInclude.Include.NON_NULL)on your Model class
All Result objects are available as static constants inside of the com.gitcodings.stack.core.result.BillingResults class.
These can be used rather than creating your own.
All endpoints in this service are considered 'privilged' as in, the user calling the endpoint must be authorized and as such must included their serialized SignedJWT inlcuded in the header of the request under the Authorization header. In the test cases you'll see that we are including these headers with JWT's for your convenience when testing.
In Spring there is a way to automatically take this header and turn it into a SignedJWT (This is already done for you by a provided filter here: JWTAuthenticationFilter). There is also a way to "ask" spring for this SignedJWT by using the @AuthenticationPrincipal SignedJWT user function parameter in the endpoint like so:
@GetMapping("/path")
public ResponseEntity<ResponseModel> endpoint(@AuthenticationPrincipal SignedJWT user)
{
...
}We can then get information out of the SignedJWT by using user.getJWTClaimsSet() and then getting the corrosponging claim by using getClaim(). Note that we get the claim by the key we used when we set the claim in the idm.
We set our userId using .claim(JWTManager.CLAIM_ID, userId) we can get that id by using getCustomClaim(JWTManager.CLAIM_ID). However this returns a type of Object if we know what the type of the specific claim is we can use the helper functions. We know we set JWTManager.CLAIM_ID as a Long so we can use getLongClaim(JWTManager.CLAIM_ID) to get it as a type of Long.
A convient function to use to get our roles our of our SignedJWT is .getStringListClaim(JWTManager.CLAIM_ROLES) which returns our user's roles as a list of Strings
Very Important For all values that deal with money, Including those in our ResponseModel We want to make sure we are returning a type of BigDecimal with a scale set to 2. The tests will fail if we do not have it set to this scale as json considers: 14.5500 different than 14.55.
Refer to Activity 5 for how to deal with BigDecimal on how to deal with BigDecimal's especially the last part with dealing with scale and rounding.
User's that have the Premium role have a diffrent UnitPrice that must be calculated by using the discount column in the table. Calculating the DiscountedUnitPrice requires we follow a very specific algorithm that is described below:
DiscountedUnitPrice= (UnitPrice* (1 - (Discount/ 100.0)))- Notice we use 100.0 (A double with the .0 at the end) this is VERY important, We want discount to become a double! We will get the wrong answer if we do not do this.
DiscountedUnitPricescale set to2withRounderingMode.DOWNTotalfor each movie =NewUnitPrice*QuantityTotalfor order is the sum of all thetotals;
Insert a given movieId into a user's cart with the given quantity
POST /cart/insert| 📨 Headers | ||
|---|---|---|
| Header | Type | Description |
| Authorization | String |
User's bearer token |
| Transaction-ID | String |
Request's transaction id from the gateway service |
| 📥 Request | ||
| Model | Example | |
movieId: Long
quantity: Integer |
{
"movieId": 4154796,
"quantity": 2
} |
|
| Key | Required | Description |
movieId | Yes | Movie Id of the movie to insert |
quantity | Yes | Quantity of the movie to add, must be between [1-10] (inclusive) |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String |
{
"result": {
"code": 3010,
"message": "Item inserted into cart"
}
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
3010 | Item inserted into cart |
❗ 400: Bad Request |
3000 | Quantity cannot be zero or less |
❗ 400: Bad Request |
3001 | Quantity exceeded max limit |
❗ 409: Conflict |
3002 | Item already in cart |
Updates the given movieId quantity in the user's cart.
POST /cart/update| 📨 Headers | ||
|---|---|---|
| Header | Type | Description |
| Authorization | String |
User's bearer token |
| Transaction-ID | String |
Request's transaction id from the gateway service |
| 📥 Request | ||
| Model | Example | |
movieId: Long
quantity: Integer |
{
"movieId": 4154796,
"quantity": 2
} |
|
| Key | Required | Description |
movieId | Yes | Movie Id of the movie to update |
quantity | Yes | Quantity of the movie to update, must be between [1-10] (inclusive) |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String |
{
"result": {
"code": 3020,
"message": "Item in cart updated"
}
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
3020 | Item in cart updated |
❗ 400: Bad Request |
3000 | Quantity cannot be zero or less |
❗ 400: Bad Request |
3001 | Quantity exceeded max limit |
❗ 409: Conflict |
3003 | Item not in cart |
Deletes the given movieId from the user's cart
DELETE /cart/delete/{movieId}| 📨 Headers | ||
|---|---|---|
| Header | Type | Description |
| Authorization | String |
User's bearer token |
| Transaction-ID | String |
Request's transaction id from the gateway service |
| 🔖 Path | ||
| Parameter | Type | Description |
| movieId | Long |
Movie id of the movie to delete from cart |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String |
{
"result": {
"code": 3030,
"message": "Item deleted from cart"
}
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
3030 | Item deleted from cart |
❗ 409: Conflict |
3003 | Item not in cart |
Retrieve's all items from the user's cart with some movie details. If the user has the Premium Role then we should report back with the "discounted rate". (Refer to Formula for applying the discount on how to do this for Premium users)
GET /cart/retrieve| 📨 Headers | ||
|---|---|---|
| Header | Type | Description |
| Authorization | String |
User's bearer token |
| Transaction-ID | String |
Request's transaction id from the gateway service |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String
total: BigDecimal (Set to a scale of 2)
items: Item[]
unitPrice: BigDecimal (Set to a scale of 2)
quantity: Integer
movieId: Long
movieTitle: String
backdropPath: String
posterPath: String |
{
"result": {
"code": 3040,
"message": "Cart items retrieved"
},
"total": 10.25,
"items": [
{
"unitPrice": 10.25,
"quantity": 2,
"movieId": 4154796,
"movieTitle": "Avengers: Endgame",
"backdropPath": "/7RyHsO4yDXtBv1zUU3mTpHeQ0d5.jpg",
"posterPath": "/or06FN3Dka5tukK1e9sl16pB3iy.jpg",
}
]
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
3040 | Cart items retrieved |
✅ 200: Ok |
3004 | Cart is empty |
Clears all movies from the user's cart
POST /cart/clear| 📨 Headers | ||
|---|---|---|
| Header | Type | Description |
| Authorization | String |
User's bearer token |
| Transaction-ID | String |
Request's transaction id from the gateway service |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String |
{
"result": {
"code": 3050,
"message": "Items have been cleared from cart"
}
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
3050 | Items have been cleared from cart |
✅ 200: Ok |
3004 | Cart is empty |
Creates a PaymentIntent with Stripe returning the newly created PaymentIntent's id and clientSecret
The PaymentIntent should be created with these three properties
- Amount: The total amount of the carts contents. (Refer to Formula for applying the discount on how to do this for
Premiumusers) - Description: The description of the movie's titles in list format (<title>, <title>, ... , <title>).
- Metadata: The key-value pair of "userId": <userId stored in the users JWT>
Refer to Activity 5 for how to deal with BigDecimal on how to deal with BigDecimal's especially the last part with dealing with scale and rounding.
GET /order/payment| 📨 Headers | ||
|---|---|---|
| Header | Type | Description |
| Authorization | String |
User's bearer token |
| Transaction-ID | String |
Request's transaction id from the gateway service |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String
paymentIntentId: String
clientSecret: String |
{
"result": {
"code": 3060,
"message": "Order Payment Intent Created"
},
"paymentIntentId": "pi_..."
"clientSecret": "pi_..."
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
3060 | Order Payment Intent Created |
✅ 200: Ok |
3004 | Cart is empty |
❗ 500: Internal Server Error |
3005 | Stripe encountered and error |
Once the order payment is accepted on the frontend we must retrieve the PaymentIntent from Stripe by calling PaymentIntent.retrieve("paymentIntentId") and verifying it.
PaymentIntent is verified by checking these two things:
- Payment Status: We verify the status by ensuring that
paymentIntent.getStatus()is"succeeded". - Correct User: We verify that the user's
userIdcalling this endpoint has the same id as the one stored in thisPaymentIntent's meta data by callingpaymentIntent.getMetadata().get("userId")
Once we verify that the payment has succeeded then we create a new billing.sale record and then populate the billing.sale_item with the contents of the user's billing.cart. Once that is done the current users cart must be cleared.
POST /order/complete| 📨 Headers | ||
|---|---|---|
| Header | Type | Description |
| Authorization | String |
User's bearer token |
| Transaction-ID | String |
Request's transaction id from the gateway service |
| 📥 Request | ||
| Model | Example | |
paymentIntentId: String |
{
"paymentIntentId": "pi_..."
} |
|
| Key | Required | Description |
paymentIntentId | Yes | paymentIntentId that was returned in /order/payment |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String |
{
"result": {
"code": 3070,
"message": "Order Completed"
}
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
3070 | Order Completed |
❗ 400: Bad Request |
3071 | The order has not yet been paid |
❗ 400: Bad Request |
3072 | The order is not meant for this user |
Return a list of sales found for the given user. To keep this endpoint simple return only the last five sales ordered by the most recent sales first.
GET /order/list| 📨 Headers | ||
|---|---|---|
| Header | Type | Description |
| Authorization | String |
User's bearer token |
| Transaction-ID | String |
Request's transaction id from the gateway service |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String
sales: Sale[]
saleId: Long
total: BigDecimal (Set to a scale of 2)
orderDate: Instant |
{
"result": {
"code": 3080,
"message": "Sales found for user"
},
"sales": [
{
"saleId": 2,
"total": 299.30,
"orderDate": "2022-01-02T20:00:00Z"
},
{
"saleId": 1,
"total": 234.45,
"orderDate": "2022-01-01T20:00:00Z"
}
]
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
3080 | Sales found for user |
✅ 200: Ok |
3081 | No sales found for user |
Return a detailed view of a given sale by its saleId.
GET /order/detail/{saleId}| 📨 Headers | ||
|---|---|---|
| Header | Type | Description |
| Authorization | String |
User's bearer token |
| Transaction-ID | String |
Request's transaction id from the gateway service |
| 🔖 Path | ||
| Parameter | Type | Description |
| saleId | Long |
Sale Id of the sale to retrieve |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String
total: BigDecimal (Set to a scale of 2)
items: Item[]
unitPrice: BigDecimal (Set to a scale of 2)
quantity: Integer
movieId: Long
movieTitle: String
backdropPath: String
posterPath: String |
{
"result": {
"code": 3090,
"message": "Sale details found for saleId"
},
"total": 10.25,
"items": [
{
"unitPrice": 10.25,
"quantity": 2,
"movieId": 4154796,
"movieTitle": "Avengers: Endgame",
"backdropPath": "/7RyHsO4yDXtBv1zUU3mTpHeQ0d5.jpg",
"posterPath": "/or06FN3Dka5tukK1e9sl16pB3iy.jpg",
}
]
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
3090 | Sale details found for saleId |
✅ 200: Ok |
3091 | No sale details found for saleId |