From c49f0c05d26c645ba58c1307e9ab9387bc504416 Mon Sep 17 00:00:00 2001 From: SimonFJ20 Date: Fri, 14 Mar 2025 13:39:53 +0100 Subject: [PATCH] add carts purchase --- backend/prepare.sql | 20 ++- backend/src/controllers/carts.c | 50 ++++++- backend/src/controllers/users.c | 10 +- backend/src/db/db.h | 13 +- backend/src/db/db_sqlite.c | 239 ++++++++++++++++++++++++++++--- backend/src/models/models.c | 60 ++++++++ backend/src/models/models.h | 20 +++ backend/src/models/models_json.h | 1 + backend/test/test.ts | 5 +- 9 files changed, 384 insertions(+), 34 deletions(-) diff --git a/backend/prepare.sql b/backend/prepare.sql index d5c2bae..aed63fd 100644 --- a/backend/prepare.sql +++ b/backend/prepare.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, - email TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, balance_dkk_cent INTEGER NOT NULL ); @@ -32,6 +32,24 @@ CREATE TABLE IF NOT EXISTS product_prices ( FOREIGN KEY(product) REFERENCES products(id) ); +CREATE TABLE IF NOT EXISTS receipts ( + id INTEGER PRIMARY KEY, + user INTEGER NOT NULL, + datetime INTEGER NOT NULL, + + FOREIGN KEY(user) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS receipt_products ( + id INTEGER PRIMARY KEY, + receipt INTEGER NOT NULL, + product_price INTEGER NOT NULL, + amount INTEGER NOT NULL, + + FOREIGN KEY(receipt) REFERENCES receipts(id) + FOREIGN KEY(product_price) REFERENCES product_prices(id) +); + INSERT OR REPLACE INTO users (name, email, password_hash, balance_dkk_cent) VALUES ('User','test@email.com','08ce0220f6d63d85c3ac313e308f4fca35ecfb850baa8ddb924cfab98137b6b18b4a8e027067cb98802757df1337246a0f3aa25c44c2b788517a871086419dcf',10000); diff --git a/backend/src/controllers/carts.c b/backend/src/controllers/carts.c index 2d2f32f..2fc60e0 100644 --- a/backend/src/controllers/carts.c +++ b/backend/src/controllers/carts.c @@ -25,12 +25,50 @@ void route_post_carts_purchase(HttpCtx* ctx) return; } - printf("product_id\tamount\n"); - for (size_t i = 0; i < req.items.size; ++i) { - printf("%ld\t\t%ld\n", - req.items.data[i].product_id, - req.items.data[i].amount); + size_t item_amount = req.items.size; + + ProductPriceVec prices; + product_price_vec_construct(&prices); + + for (size_t i = 0; i < item_amount; ++i) { + ProductPrice price; + DbRes db_res = db_product_price_of_product( + cx->db, &price, req.items.data[i].product_id); + if (db_res != DbRes_Ok) { + RESPOND_SERVER_ERROR(ctx); + goto l0_return; + } + product_price_vec_push(&prices, price); } - RESPOND_JSON(ctx, 200, "{\"ok\":true}"); + Receipt receipt = { + .id = 0, + .user_id = session->user_id, + .timestamp = NULL, + .products = (ReceiptProductVec) { 0 }, + }; + receipt_product_vec_construct(&receipt.products); + + for (size_t i = 0; i < item_amount; ++i) { + receipt_product_vec_push(&receipt.products, + (ReceiptProduct) { + .id = 0, + .receipt_id = 0, + .product_price_id = prices.data[i].id, + .amount = req.items.data[i].amount, + }); + } + + int64_t receipt_id; + DbRes db_res = db_receipt_insert(cx->db, &receipt, &receipt_id); + if (db_res != DbRes_Ok) { + RESPOND_SERVER_ERROR(ctx); + goto l0_return; + } + + RESPOND_JSON(ctx, 200, "{\"ok\":true,\"receipt_id\":%ld}", receipt_id); + +l0_return: + receipt_destroy(&receipt); + product_price_vec_destroy(&prices); } diff --git a/backend/src/controllers/users.c b/backend/src/controllers/users.c index 0f3d356..29fd5c1 100644 --- a/backend/src/controllers/users.c +++ b/backend/src/controllers/users.c @@ -27,21 +27,17 @@ void route_post_users_register(HttpCtx* ctx) return; } - Ids ids; - ids_construct(&ids); - if (db_users_with_email(cx->db, &ids, req.email) != DbRes_Ok) { + bool email_used; + if (db_user_with_email_exists(cx->db, &email_used, req.email) != DbRes_Ok) { RESPOND_SERVER_ERROR(ctx); - ids_destroy(&ids); users_register_req_destroy(&req); return; } - if (ids.size > 0) { + if (email_used) { RESPOND_BAD_REQUEST(ctx, "email in use"); - ids_destroy(&ids); users_register_req_destroy(&req); return; } - ids_destroy(&ids); char* password_hash = str_hash(req.password); diff --git a/backend/src/db/db.h b/backend/src/db/db.h index b54aef2..e8cf807 100644 --- a/backend/src/db/db.h +++ b/backend/src/db/db.h @@ -2,6 +2,7 @@ #include "../collections/collection.h" #include "../models/models.h" +#include #include DEFINE_VEC(int64_t, Ids, ids) @@ -20,11 +21,19 @@ DbRes db_user_insert(Db* db, const User* user); /// `user` field is an out parameter. DbRes db_user_with_id(Db* db, User* user, int64_t id); -/// Expects `ids` to be constructed. -DbRes db_users_with_email(Db* db, Ids* ids, const char* email); +DbRes db_user_with_email_exists(Db* db, bool* exists, const char* email); /// `user` is an out parameter. DbRes db_user_with_email(Db* db, User* user, const char* email); /// Expects `vec` to be constructed. DbRes db_product_all(Db* db, ProductVec* vec); + +/// `product_price` is an out parameter. +DbRes db_product_price_of_product( + Db* db, ProductPrice* product_price, int64_t product_id); + +/// `receipt.id`, `receipt.timestamp` and `receipt.products[i].id` +/// are ignored. +/// `id` is an out parameter. +DbRes db_receipt_insert(Db* db, const Receipt* receipt, int64_t* id); diff --git a/backend/src/db/db_sqlite.c b/backend/src/db/db_sqlite.c index f1affc3..43ba6e2 100644 --- a/backend/src/db/db_sqlite.c +++ b/backend/src/db/db_sqlite.c @@ -7,6 +7,13 @@ #include #include +#define REPORT_SQLITE3_ERROR() \ + fprintf(stderr, \ + "error: %s\n at %s:%d\n", \ + sqlite3_errmsg(connection), \ + __func__, \ + __LINE__) + static inline char* get_str_safe(sqlite3_stmt* stmt, int col) { const char* val = (const char*)sqlite3_column_text(stmt, col); @@ -146,10 +153,8 @@ l0_return: return res; } -DbRes db_users_with_email(Db* db, Ids* ids, const char* email) +DbRes db_user_with_email_exists(Db* db, bool* exists, const char* email) { - static_assert(sizeof(User) == 40, "model has changed"); - sqlite3* connection; CONNECT; DbRes res; @@ -160,9 +165,9 @@ DbRes db_users_with_email(Db* db, Ids* ids, const char* email) connection, "SELECT id FROM users WHERE email = ?", -1, &stmt, NULL); sqlite3_bind_text(stmt, 1, email, -1, NULL); - while ((sqlite_res = sqlite3_step(stmt)) == SQLITE_ROW) { - int64_t id = GET_INT(0); - ids_push(ids, id); + *exists = false; + if ((sqlite_res = sqlite3_step(stmt)) == SQLITE_ROW) { + *exists = true; } if (sqlite_res != SQLITE_DONE) { fprintf(stderr, "error: %s\n", sqlite3_errmsg(connection)); @@ -194,11 +199,7 @@ DbRes db_user_with_email(Db* db, User* user, const char* email) &stmt, NULL); if (prepare_res != SQLITE_OK) { - fprintf(stderr, - "error: %s\n at %s:%d\n", - sqlite3_errmsg(connection), - __func__, - __LINE__); + REPORT_SQLITE3_ERROR(); res = DbRes_Error; goto l0_return; } @@ -209,12 +210,7 @@ DbRes db_user_with_email(Db* db, User* user, const char* email) res = DbRes_NotFound; goto l0_return; } else if (step_res != SQLITE_ROW) { - printf("step_res = %d, email = '%s'\n", step_res, email); - fprintf(stderr, - "error: %s\n at %s:%d\n", - sqlite3_errmsg(connection), - __func__, - __LINE__); + REPORT_SQLITE3_ERROR(); res = DbRes_Error; goto l0_return; } @@ -279,3 +275,212 @@ l0_return: DISCONNECT; return res; } + +static inline DbRes get_product_price_from_product_id( + sqlite3* connection, int64_t product_id, int64_t* price) +{ + DbRes res; + sqlite3_stmt* stmt = NULL; + int prepare_res = sqlite3_prepare_v2(connection, + "SELECT price_dkk_cent FROM products WHERE id = ?", + -1, + &stmt, + NULL); + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + sqlite3_bind_int64(stmt, 1, product_id); + + int step_res = sqlite3_step(stmt); + if (step_res == SQLITE_DONE) { + res = DbRes_NotFound; + goto l0_return; + } else if (step_res != SQLITE_ROW) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + *price = GET_INT(0); + + res = DbRes_Ok; +l0_return: + if (stmt) + sqlite3_finalize(stmt); + return res; +} + +static inline DbRes insert_product_price(sqlite3* connection, + ProductPrice* product_price, + int64_t product_id, + int64_t price) +{ + DbRes res; + sqlite3_stmt* stmt = NULL; + int prepare_res = sqlite3_prepare_v2(connection, + "INSERT INTO product_prices (product, price_dkk_cent) " + "VALUES (?, ?)", + -1, + &stmt, + NULL); + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + sqlite3_bind_int64(stmt, 1, product_id); + sqlite3_bind_int64(stmt, 2, price); + + int step_res = sqlite3_step(stmt); + if (step_res != SQLITE_DONE) { + fprintf(stderr, "error: %s\n", sqlite3_errmsg(connection)); + res = DbRes_Error; + goto l0_return; + } + + int64_t id = sqlite3_last_insert_rowid(connection); + *product_price = (ProductPrice) { + .id = id, + .product_id = product_id, + .price_dkk_cent = price, + }; + + res = DbRes_Ok; +l0_return: + if (stmt) + sqlite3_finalize(stmt); + return res; +} + +DbRes db_product_price_of_product( + Db* db, ProductPrice* product_price, int64_t product_id) +{ + static_assert(sizeof(ProductPrice) == 24, "model has changed"); + + sqlite3* connection; + CONNECT; + DbRes res; + + sqlite3_stmt* stmt = NULL; + int prepare_res; + + int64_t current_price; + res = get_product_price_from_product_id( + connection, product_id, ¤t_price); + if (res != DbRes_Ok) { + goto l0_return; + } + + // find maybe existing product price + prepare_res = sqlite3_prepare_v2(connection, + "SELECT id FROM product_prices" + " WHERE product = ? AND price_dkk_cent = ?", + -1, + &stmt, + NULL); + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + sqlite3_bind_int64(stmt, 1, product_id); + sqlite3_bind_int64(stmt, 2, current_price); + + int step_res = sqlite3_step(stmt); + if (step_res == SQLITE_ROW) { + *product_price = (ProductPrice) { + .id = GET_INT(0), + .product_id = product_id, + .price_dkk_cent = current_price, + }; + } else if (step_res == SQLITE_DONE) { + insert_product_price( + connection, product_price, product_id, current_price); + } else { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + res = DbRes_Ok; +l0_return: + if (stmt) + sqlite3_finalize(stmt); + DISCONNECT; + return res; +} + +DbRes db_receipt_insert(Db* db, const Receipt* receipt, int64_t* id) +{ + static_assert(sizeof(Receipt) == 48, "model has changed"); + static_assert(sizeof(ReceiptProduct) == 32, "model has changed"); + + sqlite3* connection; + CONNECT; + DbRes res; + + sqlite3_stmt* stmt; + int prepare_res = sqlite3_prepare_v2(connection, + "INSERT INTO receipts (user, datetime) " + "VALUES (?, unixepoch('now'))", + -1, + &stmt, + NULL); + + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + sqlite3_bind_int64(stmt, 1, receipt->user_id); + + int step_res = sqlite3_step(stmt); + if (step_res != SQLITE_DONE) { + fprintf(stderr, "error: %s\n", sqlite3_errmsg(connection)); + res = DbRes_Error; + goto l0_return; + } + + int64_t receipt_id = sqlite3_last_insert_rowid(connection); + + if (id) { + *id = receipt_id; + } + + for (size_t i = 0; i < receipt->products.size; ++i) { + sqlite3_finalize(stmt); + prepare_res = sqlite3_prepare_v2(connection, + "INSERT INTO receipt_products (receipt, product_price, amount) " + "VALUES (?, ?, ?)", + -1, + &stmt, + NULL); + + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + sqlite3_bind_int64(stmt, 1, receipt_id); + sqlite3_bind_int64(stmt, 2, receipt->products.data[i].product_price_id); + sqlite3_bind_int64(stmt, 3, receipt->products.data[i].amount); + + int step_res = sqlite3_step(stmt); + if (step_res != SQLITE_DONE) { + fprintf(stderr, "error: %s\n", sqlite3_errmsg(connection)); + res = DbRes_Error; + goto l0_return; + } + } + + res = DbRes_Ok; +l0_return: + if (stmt) + sqlite3_finalize(stmt); + DISCONNECT; + return res; +} diff --git a/backend/src/models/models.c b/backend/src/models/models.c index 9bc4d61..0f1de7c 100644 --- a/backend/src/models/models.c +++ b/backend/src/models/models.c @@ -39,6 +39,20 @@ void product_price_destroy(ProductPrice* m) (void)m; } +void receipt_product_destroy(ReceiptProduct* m) +{ + static_assert(sizeof(ReceiptProduct) == 32, "model has changed"); + + (void)m; +} + +void receipt_destroy(Receipt* m) +{ + static_assert(sizeof(Receipt) == 48, "model has changed"); + + (void)m; +} + void users_register_req_destroy(UsersRegisterReq* model) { static_assert(sizeof(UsersRegisterReq) == 24, "model has changed"); @@ -156,6 +170,43 @@ char* product_price_to_json_string(const ProductPrice* m) string_destroy(&string); return result; } +char* receipt_to_json_string(const Receipt* m) +{ + static_assert(sizeof(Receipt) == 48, "model has changed"); + + String string; + string_construct(&string); + string_pushf(&string, + "{" + "\"id\":%ld," + "\"user_id\":%ld," + "\"timestamp\":\"%s\"," + "\"products\":[", + m->id, + m->user_id, + m->timestamp); + + for (size_t i = 0; i < m->products.size; ++i) { + if (i != 0) { + string_pushf(&string, ","); + } + string_pushf(&string, + "{" + "\"id\":%ld," + "\"receipt_id\":%ld," + "\"product_price_id\":%ld," + "\"amount\":%ld" + "}", + m->products.data[i].id, + m->products.data[i].receipt_id, + m->products.data[i].product_price_id, + m->products.data[i].amount); + } + string_pushf(&string, "]}"); + char* result = string_copy(&string); + string_destroy(&string); + return result; +} char* users_register_req_to_json(const UsersRegisterReq* m) { @@ -321,6 +372,13 @@ int product_price_from_json(ProductPrice* m, const JsonValue* json) return 0; } +int receipt_from_json(Receipt* m, const JsonValue* json) +{ + static_assert(sizeof(Receipt) == 48, "model has changed"); + + PANIC("not implemented"); +} + int users_register_req_from_json(UsersRegisterReq* m, const JsonValue* json) { static_assert(sizeof(UsersRegisterReq) == 24, "model has changed"); @@ -396,4 +454,6 @@ int carts_purchase_req_from_json(CartsPurchaseReq* m, const JsonValue* json) return 0; } +DEFINE_VEC_IMPL(ProductPrice, ProductPriceVec, product_price_vec, ) +DEFINE_VEC_IMPL(ReceiptProduct, ReceiptProductVec, receipt_product_vec, ) DEFINE_VEC_IMPL(CartsItem, CartsItemVec, carts_item_vec, ) diff --git a/backend/src/models/models.h b/backend/src/models/models.h index ecb18be..02b3ea7 100644 --- a/backend/src/models/models.h +++ b/backend/src/models/models.h @@ -32,10 +32,30 @@ typedef struct { int64_t price_dkk_cent; } ProductPrice; +DECLARE_VEC_TYPE(ProductPrice, ProductPriceVec, product_price_vec, ) + +typedef struct { + int64_t id; + int64_t receipt_id; + int64_t product_price_id; + int64_t amount; +} ReceiptProduct; + +DECLARE_VEC_TYPE(ReceiptProduct, ReceiptProductVec, receipt_product_vec, ) + +typedef struct { + int64_t id; + int64_t user_id; + char* timestamp; + ReceiptProductVec products; +} Receipt; + void user_destroy(User* model); void coord_destroy(Coord* model); void product_destroy(Product* model); void product_price_destroy(ProductPrice* model); +void receipt_product_destroy(ReceiptProduct* model); +void receipt_destroy(Receipt* model); // diff --git a/backend/src/models/models_json.h b/backend/src/models/models_json.h index 37265f4..6616e37 100644 --- a/backend/src/models/models_json.h +++ b/backend/src/models/models_json.h @@ -9,6 +9,7 @@ DEFINE_MODEL_JSON(User, user) DEFINE_MODEL_JSON(Coord, coord) DEFINE_MODEL_JSON(Product, product) DEFINE_MODEL_JSON(ProductPrice, product_price) +DEFINE_MODEL_JSON(Receipt, receipt) DEFINE_MODEL_JSON(UsersRegisterReq, users_register_req) DEFINE_MODEL_JSON(SessionsLoginReq, sessions_login_req) diff --git a/backend/test/test.ts b/backend/test/test.ts index 06c582c..933cbdf 100644 --- a/backend/test/test.ts +++ b/backend/test/test.ts @@ -70,12 +70,15 @@ async function testCarts(t: Deno.TestContext, token: string) { items: [ { product_id: 1, amount: 2 }, { product_id: 2, amount: 5 }, + { product_id: 2, amount: 5 }, + { product_id: 2, amount: 5 }, + { product_id: 2, amount: 5 }, ], }, { "Session-Token": token }, ); - assertEquals(res, { ok: true }); + assertEquals(res.ok, true); }); }