diff options
-rw-r--r-- | backend/migrations/2020-12-25-041256_cart_quantity/down.sql | 4 | ||||
-rw-r--r-- | backend/migrations/2020-12-25-041256_cart_quantity/up.sql | 3 | ||||
-rw-r--r-- | backend/migrations/2020-12-25-150728_transaction_date/down.sql | 4 | ||||
-rw-r--r-- | backend/migrations/2020-12-25-150728_transaction_date/up.sql | 4 | ||||
-rw-r--r-- | backend/src/bin/server.rs | 18 | ||||
-rw-r--r-- | backend/src/handlers/cart_items.rs | 131 | ||||
-rw-r--r-- | backend/src/handlers/mod.rs | 1 | ||||
-rw-r--r-- | backend/src/handlers/transaction.rs | 74 | ||||
-rw-r--r-- | backend/src/handlers/users.rs | 4 | ||||
-rw-r--r-- | backend/src/models.rs | 28 | ||||
-rw-r--r-- | backend/src/schema.rs | 2 | ||||
-rw-r--r-- | frontend/src/Cart.elm | 76 | ||||
-rw-r--r-- | frontend/src/Catalog.elm | 16 | ||||
-rw-r--r-- | frontend/src/Checkout.elm | 126 | ||||
-rw-r--r-- | frontend/src/Main.elm | 39 |
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 | |||
3 | alter table cart_items | ||
4 | drop 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 | ||
2 | alter table cart_items | ||
3 | add 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 | |||
3 | alter table transaction | ||
4 | drop 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 | |||
3 | alter table transaction | ||
4 | add 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}; | |||
5 | use diesel::r2d2::{ConnectionManager, Pool}; | 5 | use diesel::r2d2::{ConnectionManager, Pool}; |
6 | use diesel::MysqlConnection; | 6 | use diesel::MysqlConnection; |
7 | use furby::handlers::smoke::manual_hello; | 7 | use furby::handlers::smoke::manual_hello; |
8 | use furby::handlers::{cart_items, product, rating, users}; | 8 | use furby::handlers::{cart_items, product, rating, transaction, users}; |
9 | use rand::Rng; | 9 | use 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; | |||
7 | use actix_web::{web, HttpResponse, Responder}; | 7 | use actix_web::{web, HttpResponse, Responder}; |
8 | use diesel::prelude::*; | 8 | use diesel::prelude::*; |
9 | use log::{error, info}; | 9 | use log::{error, info}; |
10 | use serde::Deserialize; | 10 | use serde::Serialize; |
11 | 11 | ||
12 | pub async fn add_to_cart( | 12 | pub 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)] | ||
132 | struct UserCartItem { | ||
133 | product_item: Product, | ||
134 | quantity: i32, | ||
135 | } | ||
136 | |||
77 | pub async fn get_user_cart_items( | 137 | pub 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 | |||
173 | pub 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; | |||
2 | pub mod product; | 2 | pub mod product; |
3 | pub mod rating; | 3 | pub mod rating; |
4 | pub mod smoke; | 4 | pub mod smoke; |
5 | pub mod transaction; | ||
5 | pub mod users; | 6 | pub 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 @@ | |||
1 | use crate::models::{AddTransaction, CartItem, Customer, Product, Transaction}; | ||
2 | use crate::schema::cart_items::dsl::*; | ||
3 | use crate::schema::customer::dsl::*; | ||
4 | use crate::schema::product::dsl as prod; | ||
5 | use crate::schema::transaction::dsl::*; | ||
6 | use crate::TPool; | ||
7 | |||
8 | use actix_identity::Identity; | ||
9 | use actix_web::{web, HttpResponse, Responder}; | ||
10 | use diesel::prelude::*; | ||
11 | use log::{error, info}; | ||
12 | |||
13 | pub 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 | |||
54 | pub 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 | ||
85 | pub async fn logout(cookie: Identity) -> impl Responder { | 85 | pub 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 | ||
90 | pub async fn user_details( | 90 | pub 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 { | |||
63 | pub struct CartItem { | 71 | pub 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 { | |||
70 | pub struct AddCartItem { | 79 | pub 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)] | ||
111 | pub 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"] | ||
121 | pub 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 | ||
24 | type alias CartListing = | ||
25 | { productItem : Product | ||
26 | , quantity : Int | ||
27 | } | ||
28 | |||
29 | |||
24 | type alias Model = | 30 | type 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 | ||
36 | type Msg | 42 | type 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 | ||
43 | init : Model | 51 | init : 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 | ||
73 | decodeProduct : D.Decoder Product | 87 | decodeProduct : D.Decoder Product |
74 | decodeProduct = | 88 | decodeProduct = |
@@ -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 | ||
83 | decodeResponse : D.Decoder (List Product) | 97 | decodeResponse : D.Decoder (List CartListing) |
84 | decodeResponse = | 98 | decodeResponse = |
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 | ||
88 | removeProduct : Int -> Cmd Msg | 106 | removeProduct : Int -> Cmd Msg |
@@ -132,15 +150,45 @@ viewStatus s = | |||
132 | "Not loaded ..." | 150 | "Not loaded ..." |
133 | 151 | ||
134 | 152 | ||
135 | viewProduct : Product -> Html Msg | 153 | addToCart : Int -> Cmd Msg |
136 | viewProduct p = | 154 | addToCart 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 | |||
170 | calculateTotal : Model -> Float | ||
171 | calculateTotal 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 | |||
181 | viewCartItemListing : CartListing -> Html Msg | ||
182 | viewCartItemListing 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 | |||
188 | viewFilters model = | 188 | viewFilters 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 @@ | |||
1 | module Checkout exposing (..) | ||
2 | |||
3 | import Browser | ||
4 | import Browser.Navigation as Nav | ||
5 | import Html exposing (..) | ||
6 | import Html.Attributes exposing (..) | ||
7 | import Html.Events exposing (..) | ||
8 | import Http | ||
9 | import Json.Decode as D | ||
10 | import Json.Encode as Encode | ||
11 | import Tuple exposing (..) | ||
12 | import Url | ||
13 | import Url.Parser as P exposing ((</>), Parser, int, oneOf, s, string) | ||
14 | import Utils exposing (..) | ||
15 | |||
16 | |||
17 | type alias Model = | ||
18 | { pageStatus : Status | ||
19 | , paymentMode : String | ||
20 | , cartTotal : Float | ||
21 | } | ||
22 | |||
23 | |||
24 | type Status | ||
25 | = Loading | ||
26 | | Loaded | ||
27 | | NotLoaded | ||
28 | |||
29 | |||
30 | type Msg | ||
31 | = CheckoutPressed | ||
32 | | CheckoutSuccessful (Result Http.Error ()) | ||
33 | | AmountLoaded (Result Http.Error Float) | ||
34 | | FetchAmount | ||
35 | | PaymentModeSelected String | ||
36 | |||
37 | |||
38 | init : Model | ||
39 | init = | ||
40 | Model NotLoaded "Cash" 0 | ||
41 | |||
42 | |||
43 | update : Msg -> Model -> ( Model, Cmd Msg ) | ||
44 | update 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 | |||
71 | fetchAmount : Cmd Msg | ||
72 | fetchAmount = | ||
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 | |||
84 | tryCheckout : String -> Cmd Msg | ||
85 | tryCheckout 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 | |||
97 | viewStatus : Status -> String | ||
98 | viewStatus s = | ||
99 | case s of | ||
100 | Loading -> | ||
101 | "Loading" | ||
102 | |||
103 | Loaded -> | ||
104 | "Ready!" | ||
105 | |||
106 | NotLoaded -> | ||
107 | "Not loaded ..." | ||
108 | |||
109 | |||
110 | view : Model -> Html Msg | ||
111 | view 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 | |||
4 | import Browser.Navigation as Nav | 4 | import Browser.Navigation as Nav |
5 | import Cart | 5 | import Cart |
6 | import Catalog | 6 | import Catalog |
7 | import Checkout | ||
7 | import Html exposing (..) | 8 | import Html exposing (..) |
8 | import Html.Attributes exposing (..) | 9 | import Html.Attributes exposing (..) |
9 | import Html.Events exposing (..) | 10 | import 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)) |