aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAkshay <[email protected]>2020-12-26 05:21:46 +0000
committerAkshay <[email protected]>2020-12-26 05:21:46 +0000
commit8014def1a8da3397d78d1162f9e1b8c3f22d0322 (patch)
tree346e1de0ac6aa4ca973c1b3e5897c2c44948e5a8
parent7c6006e1abc6094b5922ab69ccfa5449b8dbbc99 (diff)
add transactions and quantities
- backend exposes endpoints to perform transactions - frontend introduces a new page to "checkout" cart
-rw-r--r--backend/migrations/2020-12-25-041256_cart_quantity/down.sql4
-rw-r--r--backend/migrations/2020-12-25-041256_cart_quantity/up.sql3
-rw-r--r--backend/migrations/2020-12-25-150728_transaction_date/down.sql4
-rw-r--r--backend/migrations/2020-12-25-150728_transaction_date/up.sql4
-rw-r--r--backend/src/bin/server.rs18
-rw-r--r--backend/src/handlers/cart_items.rs131
-rw-r--r--backend/src/handlers/mod.rs1
-rw-r--r--backend/src/handlers/transaction.rs74
-rw-r--r--backend/src/handlers/users.rs4
-rw-r--r--backend/src/models.rs28
-rw-r--r--backend/src/schema.rs2
-rw-r--r--frontend/src/Cart.elm76
-rw-r--r--frontend/src/Catalog.elm16
-rw-r--r--frontend/src/Checkout.elm126
-rw-r--r--frontend/src/Main.elm39
15 files changed, 485 insertions, 45 deletions
diff --git a/backend/migrations/2020-12-25-041256_cart_quantity/down.sql b/backend/migrations/2020-12-25-041256_cart_quantity/down.sql
new file mode 100644
index 0000000..94aec7a
--- /dev/null
+++ b/backend/migrations/2020-12-25-041256_cart_quantity/down.sql
@@ -0,0 +1,4 @@
1-- This file should undo anything in `up.sql`
2
3alter table cart_items
4drop column quantity;
diff --git a/backend/migrations/2020-12-25-041256_cart_quantity/up.sql b/backend/migrations/2020-12-25-041256_cart_quantity/up.sql
new file mode 100644
index 0000000..314c11c
--- /dev/null
+++ b/backend/migrations/2020-12-25-041256_cart_quantity/up.sql
@@ -0,0 +1,3 @@
1-- Your SQL goes here
2alter table cart_items
3add quantity integer default 1;
diff --git a/backend/migrations/2020-12-25-150728_transaction_date/down.sql b/backend/migrations/2020-12-25-150728_transaction_date/down.sql
new file mode 100644
index 0000000..18fe306
--- /dev/null
+++ b/backend/migrations/2020-12-25-150728_transaction_date/down.sql
@@ -0,0 +1,4 @@
1-- This file should undo anything in `up.sql`
2
3alter table transaction
4drop column order_date;
diff --git a/backend/migrations/2020-12-25-150728_transaction_date/up.sql b/backend/migrations/2020-12-25-150728_transaction_date/up.sql
new file mode 100644
index 0000000..76f9820
--- /dev/null
+++ b/backend/migrations/2020-12-25-150728_transaction_date/up.sql
@@ -0,0 +1,4 @@
1-- Your SQL goes here
2
3alter table transaction
4add order_date date not null default curdate();
diff --git a/backend/src/bin/server.rs b/backend/src/bin/server.rs
index 7c67e4f..310914e 100644
--- a/backend/src/bin/server.rs
+++ b/backend/src/bin/server.rs
@@ -5,7 +5,7 @@ use actix_web::{web, App, HttpServer};
5use diesel::r2d2::{ConnectionManager, Pool}; 5use diesel::r2d2::{ConnectionManager, Pool};
6use diesel::MysqlConnection; 6use diesel::MysqlConnection;
7use furby::handlers::smoke::manual_hello; 7use furby::handlers::smoke::manual_hello;
8use furby::handlers::{cart_items, product, rating, users}; 8use furby::handlers::{cart_items, product, rating, transaction, users};
9use rand::Rng; 9use rand::Rng;
10 10
11#[actix_web::main] 11#[actix_web::main]
@@ -48,6 +48,7 @@ async fn main() -> std::io::Result<()> {
48 web::scope("/user") 48 web::scope("/user")
49 .route("/existing", web::post().to(users::name_exists)) 49 .route("/existing", web::post().to(users::name_exists))
50 .route("/login", web::post().to(users::login)) 50 .route("/login", web::post().to(users::login))
51 .route("/logout", web::post().to(users::logout))
51 .route("/{uname}", web::get().to(users::user_details)) 52 .route("/{uname}", web::get().to(users::user_details))
52 .route("/new", web::post().to(users::new_user)) 53 .route("/new", web::post().to(users::new_user))
53 .route( 54 .route(
@@ -75,6 +76,10 @@ async fn main() -> std::io::Result<()> {
75 "/items", 76 "/items",
76 web::get().to(cart_items::get_user_cart_items), 77 web::get().to(cart_items::get_user_cart_items),
77 ) 78 )
79 .route(
80 "/total",
81 web::get().to(cart_items::get_user_cart_total),
82 )
78 .route("/add", web::post().to(cart_items::add_to_cart)) 83 .route("/add", web::post().to(cart_items::add_to_cart))
79 .route( 84 .route(
80 "/remove", 85 "/remove",
@@ -86,6 +91,17 @@ async fn main() -> std::io::Result<()> {
86 .route("/add", web::post().to(rating::add_rating)) 91 .route("/add", web::post().to(rating::add_rating))
87 .route("/remove", web::post().to(rating::remove_rating)), 92 .route("/remove", web::post().to(rating::remove_rating)),
88 ) 93 )
94 .service(
95 web::scope("/transaction")
96 .route(
97 "/checkout",
98 web::post().to(transaction::checkout_cart),
99 )
100 .route(
101 "/list",
102 web::get().to(transaction::list_transactions),
103 ),
104 )
89 .route("/hey", web::get().to(manual_hello)) 105 .route("/hey", web::get().to(manual_hello))
90 }) 106 })
91 .bind("127.0.0.1:7878")? 107 .bind("127.0.0.1:7878")?
diff --git a/backend/src/handlers/cart_items.rs b/backend/src/handlers/cart_items.rs
index 25baaeb..e17f4c4 100644
--- a/backend/src/handlers/cart_items.rs
+++ b/backend/src/handlers/cart_items.rs
@@ -7,7 +7,7 @@ use actix_identity::Identity;
7use actix_web::{web, HttpResponse, Responder}; 7use actix_web::{web, HttpResponse, Responder};
8use diesel::prelude::*; 8use diesel::prelude::*;
9use log::{error, info}; 9use log::{error, info};
10use serde::Deserialize; 10use serde::Serialize;
11 11
12pub async fn add_to_cart( 12pub async fn add_to_cart(
13 cookie: Identity, 13 cookie: Identity,
@@ -27,16 +27,41 @@ pub async fn add_to_cart(
27 let new_cart_item = AddCartItem { 27 let new_cart_item = AddCartItem {
28 cart_id: selected_user.id, 28 cart_id: selected_user.id,
29 product_id: item_details, 29 product_id: item_details,
30 quantity: Some(1),
30 }; 31 };
31 info!( 32 info!(
32 "cart id: {:?}, product id {:?}", 33 "cart id: {:?}, product id {:?}",
33 selected_user.id, item_details 34 selected_user.id, item_details
34 ); 35 );
35 diesel::insert_into(cart_items) 36 let current_entry = cart_items
36 .values((cart_id.eq(selected_user.id), product_id.eq(item_details))) 37 .filter(cart_id.eq(selected_user.id))
37 .execute(&conn) 38 .filter(product_id.eq(item_details))
38 .expect("Coundn't connect to DB"); 39 .limit(1)
39 HttpResponse::Ok().body("Inserted successfully!") 40 .first::<CartItem>(&conn);
41 match current_entry {
42 Ok(v) => {
43 info!("Item already present in cart, increasing quantity.");
44 let old_quantity = v.quantity.unwrap_or(1);
45 diesel::update(
46 cart_items
47 .filter(cart_id.eq(selected_user.id))
48 .filter(product_id.eq(item_details)),
49 )
50 .set(quantity.eq(old_quantity + 1))
51 .execute(&conn)
52 .unwrap();
53 return HttpResponse::Ok()
54 .body("Updated quantity successfully!");
55 }
56 Err(_) => {
57 info!("Item not present, adding to cart.");
58 diesel::insert_into(cart_items)
59 .values(new_cart_item)
60 .execute(&conn)
61 .expect("Couldn't connect to DB");
62 HttpResponse::Ok().body("Inserted successfully!")
63 }
64 }
40 } else { 65 } else {
41 error!("Unauthorized add to cart action!"); 66 error!("Unauthorized add to cart action!");
42 return HttpResponse::Unauthorized() 67 return HttpResponse::Unauthorized()
@@ -58,15 +83,44 @@ pub async fn remove_from_cart(
58 .limit(1) 83 .limit(1)
59 .first::<Customer>(&conn) 84 .first::<Customer>(&conn)
60 .expect("Couldn't connect to DB"); 85 .expect("Couldn't connect to DB");
61 86 let current_entry = cart_items
62 diesel::delete( 87 .filter(cart_id.eq(selected_user.id))
63 cart_items 88 .filter(product_id.eq(item_details))
64 .filter(cart_id.eq(selected_user.id)) 89 .limit(1)
65 .filter(product_id.eq(item_details)), 90 .first::<CartItem>(&conn);
66 ) 91 match current_entry {
67 .execute(&conn) 92 Ok(v) => {
68 .expect("Coundn't connect to DB"); 93 info!("Item already present in cart, increasing quantity.");
69 HttpResponse::Ok().body("Removed successfully!") 94 let old_quantity = v.quantity.unwrap_or(1);
95 if old_quantity == 1 {
96 diesel::delete(
97 cart_items
98 .filter(cart_id.eq(selected_user.id))
99 .filter(product_id.eq(item_details)),
100 )
101 .execute(&conn)
102 .expect("Coundn't connect to DB");
103 } else {
104 diesel::update(
105 cart_items
106 .filter(cart_id.eq(selected_user.id))
107 .filter(product_id.eq(item_details)),
108 )
109 .set(quantity.eq(old_quantity - 1))
110 .execute(&conn)
111 .unwrap();
112 return HttpResponse::Ok()
113 .body("Updated quantity successfully!");
114 }
115 return HttpResponse::Ok()
116 .body("Updated quantity successfully!");
117 }
118 Err(_) => {
119 info!("Item not present.");
120 return HttpResponse::InternalServerError()
121 .body("Item not found!");
122 }
123 }
70 } else { 124 } else {
71 error!("Unauthorized add to cart action!"); 125 error!("Unauthorized add to cart action!");
72 return HttpResponse::Unauthorized() 126 return HttpResponse::Unauthorized()
@@ -74,6 +128,12 @@ pub async fn remove_from_cart(
74 } 128 }
75} 129}
76 130
131#[derive(Serialize)]
132struct UserCartItem {
133 product_item: Product,
134 quantity: i32,
135}
136
77pub async fn get_user_cart_items( 137pub async fn get_user_cart_items(
78 cookie: Identity, 138 cookie: Identity,
79 pool: web::Data<TPool>, 139 pool: web::Data<TPool>,
@@ -92,11 +152,15 @@ pub async fn get_user_cart_items(
92 let cart_products = user_cart_items 152 let cart_products = user_cart_items
93 .into_iter() 153 .into_iter()
94 .map(|item| { 154 .map(|item| {
95 prod::product 155 let p = prod::product
96 .filter(prod::id.eq(item.product_id)) 156 .filter(prod::id.eq(item.product_id))
97 .limit(1) 157 .limit(1)
98 .first::<Product>(&conn) 158 .first::<Product>(&conn)
99 .expect("Couldn't connect to db") 159 .expect("Couldn't connect to db");
160 UserCartItem {
161 product_item: p,
162 quantity: item.quantity.unwrap_or(1),
163 }
100 }) 164 })
101 .collect::<Vec<_>>(); 165 .collect::<Vec<_>>();
102 return HttpResponse::Ok().json(&cart_products); 166 return HttpResponse::Ok().json(&cart_products);
@@ -105,3 +169,36 @@ pub async fn get_user_cart_items(
105 .body("Need to be logged in to add to cart!"); 169 .body("Need to be logged in to add to cart!");
106 } 170 }
107} 171}
172
173pub async fn get_user_cart_total(
174 cookie: Identity,
175 pool: web::Data<TPool>,
176) -> impl Responder {
177 let conn = pool.get().unwrap();
178 if let Some(uname) = cookie.identity() {
179 let selected_user = customer
180 .filter(username.eq(&uname))
181 .limit(1)
182 .first::<Customer>(&conn)
183 .expect("Couldn't connect to DB");
184 let user_cart_items = cart_items
185 .filter(cart_id.eq(selected_user.id))
186 .load::<CartItem>(&conn)
187 .expect("Couldn't connect to DB");
188 let cart_total: f32 = user_cart_items
189 .into_iter()
190 .map(|item| {
191 let p = prod::product
192 .filter(prod::id.eq(item.product_id))
193 .limit(1)
194 .first::<Product>(&conn)
195 .expect("Couldn't connect to db");
196 return p.price * item.quantity.unwrap_or(1) as f32;
197 })
198 .sum();
199 return HttpResponse::Ok().json(&cart_total);
200 } else {
201 return HttpResponse::Unauthorized()
202 .body("Need to be logged in to add to cart!");
203 }
204}
diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs
index 9416857..e4ecb3a 100644
--- a/backend/src/handlers/mod.rs
+++ b/backend/src/handlers/mod.rs
@@ -2,4 +2,5 @@ pub mod cart_items;
2pub mod product; 2pub mod product;
3pub mod rating; 3pub mod rating;
4pub mod smoke; 4pub mod smoke;
5pub mod transaction;
5pub mod users; 6pub mod users;
diff --git a/backend/src/handlers/transaction.rs b/backend/src/handlers/transaction.rs
new file mode 100644
index 0000000..1e87312
--- /dev/null
+++ b/backend/src/handlers/transaction.rs
@@ -0,0 +1,74 @@
1use crate::models::{AddTransaction, CartItem, Customer, Product, Transaction};
2use crate::schema::cart_items::dsl::*;
3use crate::schema::customer::dsl::*;
4use crate::schema::product::dsl as prod;
5use crate::schema::transaction::dsl::*;
6use crate::TPool;
7
8use actix_identity::Identity;
9use actix_web::{web, HttpResponse, Responder};
10use diesel::prelude::*;
11use log::{error, info};
12
13pub async fn checkout_cart(
14 pool: web::Data<TPool>,
15 pmt_kind: String,
16 cookie: Identity,
17) -> impl Responder {
18 let conn = pool.get().unwrap();
19 info!("Checkout cart for user: {:?}", cookie.identity());
20 if let Some(uname) = cookie.identity() {
21 let selected_user = customer
22 .filter(username.eq(&uname))
23 .limit(1)
24 .first::<Customer>(&conn)
25 .expect("Couldn't connect to DB");
26 let user_cart_items = cart_items
27 .filter(cart_id.eq(selected_user.id))
28 .load::<CartItem>(&conn)
29 .expect("Couldn't connect to DB");
30 let cart_total = user_cart_items.into_iter().fold(0., |acc, item| {
31 let item_price = prod::product
32 .filter(prod::id.eq(item.product_id))
33 .limit(1)
34 .first::<Product>(&conn)
35 .unwrap()
36 .price;
37 acc + item.quantity.unwrap_or(1) as f32 * item_price
38 });
39 let transaction_entry = AddTransaction {
40 customer_id: Some(selected_user.id),
41 amount: cart_total,
42 payment_type: pmt_kind,
43 };
44 diesel::insert_into(transaction)
45 .values(transaction_entry)
46 .execute(&conn)
47 .expect("Coundn't connect to DB");
48 return HttpResponse::Ok().body("Transaction performed successfully");
49 } else {
50 return HttpResponse::Unauthorized().body("Login first");
51 }
52}
53
54pub async fn list_transactions(
55 pool: web::Data<TPool>,
56 cookie: Identity,
57) -> impl Responder {
58 let conn = pool.get().unwrap();
59 if let Some(uname) = cookie.identity() {
60 let selected_user = customer
61 .filter(username.eq(&uname))
62 .limit(1)
63 .first::<Customer>(&conn)
64 .expect("Couldn't connect to DB");
65 let user_transactions = transaction
66 .filter(customer_id.eq(selected_user.id))
67 .load::<Transaction>(&conn)
68 .expect("Couldn't connect to DB");
69 return HttpResponse::Ok().json(&user_transactions);
70 } else {
71 return HttpResponse::Unauthorized()
72 .body("Need to be logged in to add to cart!");
73 }
74}
diff --git a/backend/src/handlers/users.rs b/backend/src/handlers/users.rs
index 24fb591..a043c1f 100644
--- a/backend/src/handlers/users.rs
+++ b/backend/src/handlers/users.rs
@@ -58,7 +58,7 @@ pub async fn login(
58 login_details: web::Json<Login>, 58 login_details: web::Json<Login>,
59) -> impl Responder { 59) -> impl Responder {
60 info!("Login hit"); 60 info!("Login hit");
61 if let Some(uname) = cookie.identity() { 61 if cookie.identity().is_some() {
62 info!("Found existing cookie: {:?}", cookie.identity()); 62 info!("Found existing cookie: {:?}", cookie.identity());
63 return HttpResponse::Ok().finish(); 63 return HttpResponse::Ok().finish();
64 } 64 }
@@ -84,7 +84,7 @@ pub async fn login(
84 84
85pub async fn logout(cookie: Identity) -> impl Responder { 85pub async fn logout(cookie: Identity) -> impl Responder {
86 cookie.forget(); 86 cookie.forget();
87 HttpResponse::Found().header("location", "/").finish() 87 HttpResponse::Ok().body("Successful logout.")
88} 88}
89 89
90pub async fn user_details( 90pub async fn user_details(
diff --git a/backend/src/models.rs b/backend/src/models.rs
index a104209..bf531ad 100644
--- a/backend/src/models.rs
+++ b/backend/src/models.rs
@@ -35,6 +35,8 @@ pub struct Product {
35 pub kind: Option<String>, 35 pub kind: Option<String>,
36 pub price: f32, 36 pub price: f32,
37 pub description: Option<String>, 37 pub description: Option<String>,
38 pub src: Option<String>,
39 pub ios_src: Option<String>,
38} 40}
39 41
40#[derive(Insertable, Deserialize)] 42#[derive(Insertable, Deserialize)]
@@ -48,6 +50,12 @@ pub struct NewProduct {
48 50
49 #[serde(skip_serializing_if = "Option::is_none")] 51 #[serde(skip_serializing_if = "Option::is_none")]
50 pub description: Option<String>, 52 pub description: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub src: Option<String>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub ios_src: Option<String>,
51} 59}
52 60
53#[derive(Deserialize)] 61#[derive(Deserialize)]
@@ -63,6 +71,7 @@ pub struct UpdateProduct {
63pub struct CartItem { 71pub struct CartItem {
64 pub cart_id: i32, 72 pub cart_id: i32,
65 pub product_id: i32, 73 pub product_id: i32,
74 pub quantity: Option<i32>,
66} 75}
67 76
68#[derive(Insertable, Deserialize)] 77#[derive(Insertable, Deserialize)]
@@ -70,6 +79,7 @@ pub struct CartItem {
70pub struct AddCartItem { 79pub struct AddCartItem {
71 pub cart_id: i32, 80 pub cart_id: i32,
72 pub product_id: i32, 81 pub product_id: i32,
82 pub quantity: Option<i32>,
73} 83}
74 84
75/* Rating */ 85/* Rating */
@@ -95,3 +105,21 @@ pub struct AddRating {
95 pub product_id: i32, 105 pub product_id: i32,
96 pub customer_id: i32, 106 pub customer_id: i32,
97} 107}
108
109/* Transaction */
110#[derive(Queryable, Serialize)]
111pub struct Transaction {
112 pub id: i32,
113 pub payment_type: String,
114 pub amount: f32,
115 pub customer_id: Option<i32>,
116 pub order_date: NaiveDate,
117}
118
119#[derive(Insertable, Deserialize)]
120#[table_name = "transaction"]
121pub struct AddTransaction {
122 pub payment_type: String,
123 pub amount: f32,
124 pub customer_id: Option<i32>,
125}
diff --git a/backend/src/schema.rs b/backend/src/schema.rs
index f08221a..1419bc0 100644
--- a/backend/src/schema.rs
+++ b/backend/src/schema.rs
@@ -2,6 +2,7 @@ table! {
2 cart_items (cart_id, product_id) { 2 cart_items (cart_id, product_id) {
3 cart_id -> Integer, 3 cart_id -> Integer,
4 product_id -> Integer, 4 product_id -> Integer,
5 quantity -> Nullable<Integer>,
5 } 6 }
6} 7}
7 8
@@ -43,6 +44,7 @@ table! {
43 payment_type -> Varchar, 44 payment_type -> Varchar,
44 amount -> Float, 45 amount -> Float,
45 customer_id -> Nullable<Integer>, 46 customer_id -> Nullable<Integer>,
47 order_date -> Date,
46 } 48 }
47} 49}
48 50
diff --git a/frontend/src/Cart.elm b/frontend/src/Cart.elm
index a1750f6..58fb72e 100644
--- a/frontend/src/Cart.elm
+++ b/frontend/src/Cart.elm
@@ -21,9 +21,15 @@ type alias Product =
21 } 21 }
22 22
23 23
24type alias CartListing =
25 { productItem : Product
26 , quantity : Int
27 }
28
29
24type alias Model = 30type alias Model =
25 { pageStatus : Status 31 { pageStatus : Status
26 , products : List Product 32 , products : List CartListing
27 } 33 }
28 34
29 35
@@ -34,10 +40,12 @@ type Status
34 40
35 41
36type Msg 42type Msg
37 = CartLoaded (Result Http.Error (List Product)) 43 = CartLoaded (Result Http.Error (List CartListing))
38 | FetchCartItems 44 | FetchCartItems
39 | RemoveFromCart Int 45 | RemoveFromCart Int
40 | CartItemRemoved (Result Http.Error ()) 46 | CartItemRemoved (Result Http.Error ())
47 | AddToCartSuccess (Result Http.Error ())
48 | AddToCartPressed Int
41 49
42 50
43init : Model 51init : Model
@@ -69,6 +77,12 @@ update msg model =
69 FetchCartItems -> 77 FetchCartItems ->
70 ( { model | pageStatus = Loading }, fetchCartItems ) 78 ( { model | pageStatus = Loading }, fetchCartItems )
71 79
80 AddToCartPressed id ->
81 ( model, addToCart id )
82
83 AddToCartSuccess _ ->
84 ( { model | pageStatus = Loading }, fetchCartItems )
85
72 86
73decodeProduct : D.Decoder Product 87decodeProduct : D.Decoder Product
74decodeProduct = 88decodeProduct =
@@ -80,9 +94,13 @@ decodeProduct =
80 (D.field "description" (D.nullable D.string)) 94 (D.field "description" (D.nullable D.string))
81 95
82 96
83decodeResponse : D.Decoder (List Product) 97decodeResponse : D.Decoder (List CartListing)
84decodeResponse = 98decodeResponse =
85 D.list decodeProduct 99 D.list
100 (D.map2 CartListing
101 (D.field "product_item" decodeProduct)
102 (D.field "quantity" D.int)
103 )
86 104
87 105
88removeProduct : Int -> Cmd Msg 106removeProduct : Int -> Cmd Msg
@@ -132,15 +150,45 @@ viewStatus s =
132 "Not loaded ..." 150 "Not loaded ..."
133 151
134 152
135viewProduct : Product -> Html Msg 153addToCart : Int -> Cmd Msg
136viewProduct p = 154addToCart id =
155 let
156 _ =
157 Debug.log "err" <| "adding to cart: " ++ String.fromInt id
158 in
159 Http.riskyRequest
160 { method = "POST"
161 , headers = []
162 , url = "http://127.0.0.1:7878/cart/add"
163 , body = Http.stringBody "applcation/json" <| String.fromInt <| id
164 , expect = Http.expectWhatever AddToCartSuccess
165 , timeout = Nothing
166 , tracker = Nothing
167 }
168
169
170calculateTotal : Model -> Float
171calculateTotal model =
172 let
173 items =
174 model.products
175 in
176 items
177 |> List.map (\i -> toFloat i.quantity * i.productItem.price)
178 |> List.foldl (+) 0
179
180
181viewCartItemListing : CartListing -> Html Msg
182viewCartItemListing listing =
137 div [] 183 div []
138 [ text p.name 184 [ text listing.productItem.name
139 , div [] [ text <| Maybe.withDefault "" p.kind ] 185 , div [] [ text <| Maybe.withDefault "" listing.productItem.kind ]
140 , div [] [ text <| Maybe.withDefault "" p.description ] 186 , div [] [ text <| Maybe.withDefault "" listing.productItem.description ]
141 , div [] [ text <| String.fromFloat p.price ] 187 , div [] [ text <| String.fromFloat listing.productItem.price ]
142 , div [] [ button [ onClick (RemoveFromCart p.id) ] [ text "Remove" ] ] 188 , div [] [ text <| String.fromInt listing.quantity ]
143 , div [] [ a [ href ("/product/" ++ String.fromInt p.id) ] [ text "View Product" ] ] 189 , div [] [ button [ onClick (AddToCartPressed listing.productItem.id) ] [ text "Add" ] ]
190 , div [] [ button [ onClick (RemoveFromCart listing.productItem.id) ] [ text "Remove" ] ]
191 , div [] [ a [ href ("/product/" ++ String.fromInt listing.productItem.id) ] [ text "View Product" ] ]
144 ] 192 ]
145 193
146 194
@@ -154,11 +202,13 @@ view model =
154 div [] 202 div []
155 [ let 203 [ let
156 cart = 204 cart =
157 List.map viewProduct model.products 205 List.map viewCartItemListing model.products
158 in 206 in
159 if List.isEmpty cart then 207 if List.isEmpty cart then
160 text "No items in cart" 208 text "No items in cart"
161 209
162 else 210 else
163 ul [] cart 211 ul [] cart
212 , calculateTotal model |> String.fromFloat |> text
213 , a [ href "/checkout" ] [ text "Checkout" ]
164 ] 214 ]
diff --git a/frontend/src/Catalog.elm b/frontend/src/Catalog.elm
index 6882c73..d00cb92 100644
--- a/frontend/src/Catalog.elm
+++ b/frontend/src/Catalog.elm
@@ -188,13 +188,13 @@ viewFilters : Model -> Html Msg
188viewFilters model = 188viewFilters model =
189 let 189 let
190 priceRange = 190 priceRange =
191 range 0 50000 5000 191 range 0 55000 5000
192 192
193 ratingRange = 193 ratingRange =
194 List.range 1 5 194 range 1 6 1
195 195
196 viewRange = 196 viewRange default scale =
197 List.map (\i -> option [] [ text <| String.fromInt i ]) 197 List.map (\i -> option [ selected (i == default) ] [ text <| String.fromInt i ]) scale
198 198
199 inp = 199 inp =
200 Maybe.withDefault 0 << String.toFloat 200 Maybe.withDefault 0 << String.toFloat
@@ -202,15 +202,15 @@ viewFilters model =
202 div [] 202 div []
203 [ div [] 203 [ div []
204 [ text "Price" 204 [ text "Price"
205 , select [ onInput (ChangePriceLower << inp) ] (viewRange priceRange) 205 , select [ onInput (ChangePriceLower << inp) ] (viewRange 0 priceRange)
206 , text "to" 206 , text "to"
207 , select [ onInput (ChangePriceUpper << inp) ] (viewRange priceRange) 207 , select [ onInput (ChangePriceUpper << inp) ] (viewRange 50000 priceRange)
208 ] 208 ]
209 , div [] 209 , div []
210 [ text "Rating" 210 [ text "Rating"
211 , select [ onInput (ChangeRatingLower << inp) ] (viewRange ratingRange) 211 , select [ onInput (ChangeRatingLower << inp) ] (viewRange 1 ratingRange)
212 , text "to" 212 , text "to"
213 , select [ onInput (ChangeRatingUpper << inp) ] (viewRange ratingRange) 213 , select [ onInput (ChangeRatingUpper << inp) ] (viewRange 5 ratingRange)
214 ] 214 ]
215 ] 215 ]
216 216
diff --git a/frontend/src/Checkout.elm b/frontend/src/Checkout.elm
new file mode 100644
index 0000000..c60da0d
--- /dev/null
+++ b/frontend/src/Checkout.elm
@@ -0,0 +1,126 @@
1module Checkout exposing (..)
2
3import Browser
4import Browser.Navigation as Nav
5import Html exposing (..)
6import Html.Attributes exposing (..)
7import Html.Events exposing (..)
8import Http
9import Json.Decode as D
10import Json.Encode as Encode
11import Tuple exposing (..)
12import Url
13import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string)
14import Utils exposing (..)
15
16
17type alias Model =
18 { pageStatus : Status
19 , paymentMode : String
20 , cartTotal : Float
21 }
22
23
24type Status
25 = Loading
26 | Loaded
27 | NotLoaded
28
29
30type Msg
31 = CheckoutPressed
32 | CheckoutSuccessful (Result Http.Error ())
33 | AmountLoaded (Result Http.Error Float)
34 | FetchAmount
35 | PaymentModeSelected String
36
37
38init : Model
39init =
40 Model NotLoaded "Cash" 0
41
42
43update : Msg -> Model -> ( Model, Cmd Msg )
44update msg model =
45 case msg of
46 CheckoutPressed ->
47 ( model, tryCheckout model.paymentMode )
48
49 CheckoutSuccessful _ ->
50 ( model, Cmd.none )
51
52 AmountLoaded res ->
53 case res of
54 Ok v ->
55 ( { model | cartTotal = v }, Cmd.none )
56
57 Err _ ->
58 ( { model | pageStatus = NotLoaded }, Cmd.none )
59
60 FetchAmount ->
61 let
62 _ =
63 Debug.log "err" "fetching checkout amount"
64 in
65 ( { model | pageStatus = Loading }, fetchAmount )
66
67 PaymentModeSelected s ->
68 ( { model | paymentMode = s }, Cmd.none )
69
70
71fetchAmount : Cmd Msg
72fetchAmount =
73 Http.riskyRequest
74 { method = "GET"
75 , headers = []
76 , url = "http://127.0.0.1:7878/cart/total"
77 , body = Http.emptyBody
78 , expect = Http.expectJson AmountLoaded D.float
79 , timeout = Nothing
80 , tracker = Nothing
81 }
82
83
84tryCheckout : String -> Cmd Msg
85tryCheckout pm =
86 Http.riskyRequest
87 { method = "POST"
88 , headers = []
89 , url = "http://127.0.0.1:7878/transaction/checkout"
90 , body = Http.stringBody "application/json" pm
91 , expect = Http.expectWhatever CheckoutSuccessful
92 , timeout = Nothing
93 , tracker = Nothing
94 }
95
96
97viewStatus : Status -> String
98viewStatus s =
99 case s of
100 Loading ->
101 "Loading"
102
103 Loaded ->
104 "Ready!"
105
106 NotLoaded ->
107 "Not loaded ..."
108
109
110view : Model -> Html Msg
111view model =
112 case model.pageStatus of
113 Loading ->
114 div [] [ text <| viewStatus Loading ]
115
116 _ ->
117 div []
118 [ div [] [ text <| String.fromFloat <| model.cartTotal ]
119 , select []
120 [ option [ onInput PaymentModeSelected ] [ text "Cash" ]
121 , option [ onInput PaymentModeSelected ] [ text "Debit Card" ]
122 , option [ onInput PaymentModeSelected ] [ text "Credit Card" ]
123 ]
124 , div [] [ a [ href "/cart" ] [ text "Cancel" ] ]
125 , div [] [ button [ onClick CheckoutPressed ] [ text "Confirm and Pay" ] ]
126 ]
diff --git a/frontend/src/Main.elm b/frontend/src/Main.elm
index bf1583c..f1883a1 100644
--- a/frontend/src/Main.elm
+++ b/frontend/src/Main.elm
@@ -4,6 +4,7 @@ import Browser
4import Browser.Navigation as Nav 4import Browser.Navigation as Nav
5import Cart 5import Cart
6import Catalog 6import Catalog
7import Checkout
7import Html exposing (..) 8import Html exposing (..)
8import Html.Attributes exposing (..) 9import Html.Attributes exposing (..)
9import Html.Events exposing (..) 10import Html.Events exposing (..)
@@ -43,6 +44,7 @@ type Route
43 | CatalogPage 44 | CatalogPage
44 | CartPage 45 | CartPage
45 | ProductPage Int 46 | ProductPage Int
47 | CheckoutPage
46 | NotFoundPage 48 | NotFoundPage
47 49
48 50
@@ -54,9 +56,8 @@ parseRoute =
54 , P.map CatalogPage (P.s "catalog") 56 , P.map CatalogPage (P.s "catalog")
55 , P.map CartPage (P.s "cart") 57 , P.map CartPage (P.s "cart")
56 , P.map SignupPage (P.s "signup") 58 , P.map SignupPage (P.s "signup")
59 , P.map CheckoutPage (P.s "checkout")
57 , P.map ProductPage (P.s "product" </> P.int) 60 , P.map ProductPage (P.s "product" </> P.int)
58
59 --, P.map ProductPage (P.s "product" </> int)
60 ] 61 ]
61 62
62 63
@@ -69,6 +70,7 @@ type alias Model =
69 , productModel : Product.Model 70 , productModel : Product.Model
70 , signupModel : Signup.Model 71 , signupModel : Signup.Model
71 , cartModel : Cart.Model 72 , cartModel : Cart.Model
73 , checkoutModel : Checkout.Model
72 } 74 }
73 75
74 76
@@ -92,8 +94,11 @@ init flags url key =
92 94
93 cart = 95 cart =
94 Cart.init 96 Cart.init
97
98 checkout =
99 Checkout.init
95 in 100 in
96 ( Model key url start login catalog product signup cart, Cmd.none ) 101 ( Model key url start login catalog product signup cart checkout, Cmd.none )
97 102
98 103
99 104
@@ -108,6 +113,7 @@ type Msg
108 | ProductMessage Product.Msg 113 | ProductMessage Product.Msg
109 | SignupMessage Signup.Msg 114 | SignupMessage Signup.Msg
110 | CartMessage Cart.Msg 115 | CartMessage Cart.Msg
116 | CheckoutMessage Checkout.Msg
111 | LogoutPressed 117 | LogoutPressed
112 | LogoutSuccess (Result Http.Error ()) 118 | LogoutSuccess (Result Http.Error ())
113 119
@@ -127,7 +133,7 @@ update msg model =
127 ( model, tryLogout ) 133 ( model, tryLogout )
128 134
129 LogoutSuccess _ -> 135 LogoutSuccess _ ->
130 ( model, Nav.replaceUrl model.key "/login" ) 136 ( { model | loginModel = Login.init }, Nav.replaceUrl model.key "/login" )
131 137
132 UrlChanged url -> 138 UrlChanged url ->
133 let 139 let
@@ -155,6 +161,16 @@ update msg model =
155 in 161 in
156 ( { model | location = CartPage }, cmd ) 162 ( { model | location = CartPage }, cmd )
157 163
164 Just CheckoutPage ->
165 let
166 _ =
167 Debug.log "err" "loading checkout page ..."
168
169 cmd =
170 Cmd.map CheckoutMessage Checkout.fetchAmount
171 in
172 ( { model | location = CheckoutPage }, cmd )
173
158 Just p -> 174 Just p ->
159 ( { model | location = p }, Cmd.none ) 175 ( { model | location = p }, Cmd.none )
160 176
@@ -205,6 +221,16 @@ update msg model =
205 in 221 in
206 ( { model | cartModel = cmn }, Cmd.map CartMessage cmd ) 222 ( { model | cartModel = cmn }, Cmd.map CartMessage cmd )
207 223
224 CheckoutMessage cm ->
225 let
226 ( cmn, cmd ) =
227 Checkout.update cm model.checkoutModel
228
229 _ =
230 Debug.log "err" "received checkout message ..."
231 in
232 ( { model | checkoutModel = cmn }, Cmd.map CheckoutMessage cmd )
233
208 ProductMessage pm -> 234 ProductMessage pm ->
209 let 235 let
210 ( pmn, cmd ) = 236 ( pmn, cmd ) =
@@ -292,6 +318,11 @@ view model =
292 , body = pageWrap model (Html.map CartMessage (Cart.view model.cartModel)) 318 , body = pageWrap model (Html.map CartMessage (Cart.view model.cartModel))
293 } 319 }
294 320
321 CheckoutPage ->
322 { title = "Checkout"
323 , body = pageWrap model (Html.map CheckoutMessage (Checkout.view model.checkoutModel))
324 }
325
295 ProductPage item -> 326 ProductPage item ->
296 { title = "Product " ++ String.fromInt item 327 { title = "Product " ++ String.fromInt item
297 , body = pageWrap model (Html.map ProductMessage (Product.view model.productModel)) 328 , body = pageWrap model (Html.map ProductMessage (Product.view model.productModel))