From 2d74a872fe54db08710da7465b9d1cedacc0b963 Mon Sep 17 00:00:00 2001 From: SimonFJ20 Date: Fri, 14 Mar 2025 16:17:31 +0100 Subject: [PATCH] get one receipt --- backend/src/controllers/controllers.h | 8 +- backend/src/controllers/general.c | 11 +++ backend/src/controllers/receipts.c | 107 +++++++++++++++++++++ backend/src/db/db.h | 8 ++ backend/src/db/db_sqlite.c | 130 +++++++++++++++++++++++++- backend/src/http/client.c | 4 +- backend/src/http/http.h | 1 + backend/src/http/server.c | 5 + backend/src/main.c | 2 + backend/src/util/str.c | 12 ++- backend/src/util/str.h | 2 + backend/test/test.ts | 33 ++++++- 12 files changed, 312 insertions(+), 11 deletions(-) create mode 100644 backend/src/controllers/receipts.c diff --git a/backend/src/controllers/controllers.h b/backend/src/controllers/controllers.h index 9e61efe..e5cf5fb 100644 --- a/backend/src/controllers/controllers.h +++ b/backend/src/controllers/controllers.h @@ -45,14 +45,17 @@ void route_post_sessions_login(HttpCtx* ctx); void route_post_sessions_logout(HttpCtx* ctx); void route_get_sessions_user(HttpCtx* ctx); +void route_get_receipt(HttpCtx* ctx); + const Session* header_session(HttpCtx* ctx); const Session* middleware_session(HttpCtx* ctx); #define RESPOND(HTTP_CTX, STATUS, MIME_TYPE, ...) \ { \ HttpCtx* _ctx = (HTTP_CTX); \ - char _body[8192]; \ - snprintf(_body, 8192 - 1, __VA_ARGS__); \ + size_t _body_size = (size_t)snprintf(NULL, 0, __VA_ARGS__); \ + char* _body = calloc(_body_size + 1, sizeof(char)); \ + sprintf(_body, __VA_ARGS__); \ \ char content_length[24] = { 0 }; \ snprintf(content_length, 24 - 1, "%ld", strlen(_body)); \ @@ -61,6 +64,7 @@ const Session* middleware_session(HttpCtx* ctx); http_ctx_res_headers_set(_ctx, "Content-Length", content_length); \ \ http_ctx_respond(_ctx, (STATUS), _body); \ + free(_body); \ } #define RESPOND_HTML(HTTP_CTX, STATUS, ...) \ diff --git a/backend/src/controllers/general.c b/backend/src/controllers/general.c index db4a0d7..0d33d70 100644 --- a/backend/src/controllers/general.c +++ b/backend/src/controllers/general.c @@ -1,6 +1,7 @@ #include "../http/http.h" #include "../models/models_json.h" #include "controllers.h" +#include void route_get_index(HttpCtx* ctx) { @@ -40,6 +41,16 @@ l0_return: void route_get_not_found(HttpCtx* ctx) { + if (http_ctx_req_headers_has(ctx, "Accept")) { + const char* accept = http_ctx_req_headers_get(ctx, "Accept"); + if (strcmp(accept, "application/json") == 0) { + RESPOND_JSON(ctx, + 404, + "{\"ok\":false,\"msg\":\"404 Not Found\",\"path\":\"%s\"}", + http_ctx_req_path(ctx)); + return; + } + } RESPOND_HTML(ctx, 404, " +#include + +typedef struct { + StrSlice key; + StrSlice value; +} QueryParamEntry; + +DEFINE_VEC(QueryParamEntry, QueryParamVec, query_param_vec) + +typedef struct { + QueryParamVec vec; +} QueryParams; + +QueryParams parse_query_params(const char* query) +{ + QueryParams result = { + .vec = (QueryParamVec) { 0 }, + }; + query_param_vec_construct(&result.vec); + + StrSplitter params = str_splitter(query, strlen(query), "&"); + StrSlice param; + while ((param = str_split_next(¶ms)).len != 0) { + StrSplitter left_right = str_splitter(param.ptr, param.len, "="); + StrSlice key = str_split_next(&left_right); + StrSlice value = str_split_next(&left_right); + + query_param_vec_push(&result.vec, (QueryParamEntry) { key, value }); + } + + return result; +} + +void query_params_destroy(QueryParams* query_params) +{ + query_param_vec_destroy(&query_params->vec); +} + +const char* query_params_get(const QueryParams* query_params, const char* key) +{ + size_t key_len = strlen(key); + for (size_t i = 0; i < query_params->vec.size; ++i) { + const QueryParamEntry* entry = &query_params->vec.data[i]; + if (key_len == entry->key.len && strcmp(key, entry->key.ptr)) { + return str_slice_copy(&entry->value); + } + } + return NULL; +} + +void route_get_receipt(HttpCtx* ctx) +{ + Cx* cx = http_ctx_user_ctx(ctx); + const Session* session = middleware_session(ctx); + if (!session) + return; + + const char* query = http_ctx_req_query(ctx); + QueryParams params = parse_query_params(query); + const char* receipt_id_str = query_params_get(¶ms, "receipt_id"); + query_params_destroy(¶ms); + if (!receipt_id_str) { + RESPOND_BAD_REQUEST(ctx, "no receipt_id parameter"); + return; + } + + int64_t receipt_id = strtol(receipt_id_str, NULL, 10); + + Receipt receipt; + DbRes db_rizz = db_receipt_with_id(cx->db, &receipt, receipt_id); + if (db_rizz != DbRes_Ok) { + RESPOND_BAD_REQUEST(ctx, "receipt not found"); + return; + } + + ProductPriceVec product_prices = { 0 }; + product_price_vec_construct(&product_prices); + db_receipt_prices(cx->db, &product_prices, receipt_id); + + String res; + string_construct(&res); + + char* receipt_str = receipt_to_json_string(&receipt); + + string_pushf( + &res, "{\"ok\":true,\"receipt\":%s,\"product_prices\":[", receipt_str); + + for (size_t i = 0; i < product_prices.size; ++i) { + if (i != 0) { + string_pushf(&res, ","); + } + char* product_price_str + = product_price_to_json_string(&product_prices.data[i]); + string_pushf(&res, "%s", product_price_str); + free(product_price_str); + } + string_pushf(&res, "]}"); + + RESPOND_JSON(ctx, 200, "%s", res.data); + string_destroy(&res); + product_price_vec_destroy(&product_prices); + receipt_destroy(&receipt); +} diff --git a/backend/src/db/db.h b/backend/src/db/db.h index e8cf807..8dd4193 100644 --- a/backend/src/db/db.h +++ b/backend/src/db/db.h @@ -37,3 +37,11 @@ DbRes db_product_price_of_product( /// are ignored. /// `id` is an out parameter. DbRes db_receipt_insert(Db* db, const Receipt* receipt, int64_t* id); + +/// `receipt` field is an out parameter. +DbRes db_receipt_with_id(Db* db, Receipt* receipt, int64_t id); + +/// `product_prices` field is an out parameter. +/// Expects `product_prices` to be constructed. +DbRes db_receipt_prices( + Db* db, ProductPriceVec* product_prices, int64_t receipt_id); diff --git a/backend/src/db/db_sqlite.c b/backend/src/db/db_sqlite.c index 43ba6e2..13d9f0e 100644 --- a/backend/src/db/db_sqlite.c +++ b/backend/src/db/db_sqlite.c @@ -373,7 +373,6 @@ DbRes db_product_price_of_product( 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 = ?", @@ -484,3 +483,132 @@ l0_return: DISCONNECT; return res; } + +DbRes db_receipt_with_id(Db* db, Receipt* receipt, int64_t id) +{ + static_assert(sizeof(Receipt) == 48, "model has changed"); + + sqlite3* connection; + CONNECT; + DbRes res; + sqlite3_stmt* stmt = NULL; + + int prepare_res = sqlite3_prepare_v2(connection, + "SELECT id, user, datetime(datetime) FROM receipts WHERE id = ?", + -1, + &stmt, + NULL); + + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + sqlite3_bind_int64(stmt, 1, 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; + } + + *receipt = (Receipt) { + .id = GET_INT(0), + .user_id = GET_INT(1), + .timestamp = GET_STR(2), + .products = (ReceiptProductVec) { 0 }, + }; + + receipt_product_vec_construct(&receipt->products); + + prepare_res = sqlite3_prepare_v2(connection, + "SELECT id, receipt, product_price, amount FROM receipt_products" + " WHERE receipt = ?", + -1, + &stmt, + NULL); + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + sqlite3_bind_int64(stmt, 1, receipt->id); + + int sqlite_res; + while ((sqlite_res = sqlite3_step(stmt)) == SQLITE_ROW) { + ReceiptProduct product = { + .id = GET_INT(0), + .receipt_id = GET_INT(1), + .product_price_id = GET_INT(2), + .amount = GET_INT(3), + }; + receipt_product_vec_push(&receipt->products, product); + } + if (sqlite_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; +} + +DbRes db_receipt_prices( + Db* db, ProductPriceVec* product_prices, int64_t receipt_id) +{ + static_assert(sizeof(ProductPrice) == 24, "model has changed"); + + sqlite3* connection; + CONNECT; + DbRes res; + sqlite3_stmt* stmt = NULL; + + int prepare_res = sqlite3_prepare_v2(connection, + "SELECT product_prices.id, product_prices.product," + " product_prices.price_dkk_cent" + " FROM receipt_products JOIN product_prices" + " ON product_prices.id = product_price" + " AND receipt_products.receipt = ?", + -1, + &stmt, + NULL); + + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + sqlite3_bind_int64(stmt, 1, receipt_id); + + int sqlite_res; + while ((sqlite_res = sqlite3_step(stmt)) == SQLITE_ROW) { + ProductPrice product_price = { + .id = GET_INT(0), + .product_id = GET_INT(1), + .price_dkk_cent = GET_INT(2), + }; + product_price_vec_push(product_prices, product_price); + } + if (sqlite_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/http/client.c b/backend/src/http/client.c index f05cd4e..240c132 100644 --- a/backend/src/http/client.c +++ b/backend/src/http/client.c @@ -143,12 +143,12 @@ static inline int parse_request_header(Client* client, Request* request) char* query = NULL; if (path_len < uri_str.len) { size_t query_len = 0; - while (path_len + query_len < uri_str.len + while (path_len + query_len + 1 < uri_str.len && uri_str.ptr[path_len + query_len] != '#') { query_len += 1; } query = calloc(query_len + 1, sizeof(char)); - strncpy(query, &uri_str.ptr[path_len], query_len); + strncpy(query, &uri_str.ptr[path_len + 1], query_len); query[query_len] = '\0'; } diff --git a/backend/src/http/http.h b/backend/src/http/http.h index 0c16219..00ca27a 100644 --- a/backend/src/http/http.h +++ b/backend/src/http/http.h @@ -32,6 +32,7 @@ void* http_ctx_user_ctx(HttpCtx* ctx); const char* http_ctx_req_path(HttpCtx* ctx); bool http_ctx_req_headers_has(HttpCtx* ctx, const char* key); const char* http_ctx_req_headers_get(HttpCtx* ctx, const char* key); +const char* http_ctx_req_query(HttpCtx* ctx); const char* http_ctx_req_body(HttpCtx* ctx); void http_ctx_res_headers_set(HttpCtx* ctx, const char* key, const char* value); void http_ctx_respond(HttpCtx* ctx, int status, const char* body); diff --git a/backend/src/http/server.c b/backend/src/http/server.c index 981244c..fd257d1 100644 --- a/backend/src/http/server.c +++ b/backend/src/http/server.c @@ -155,6 +155,11 @@ const char* http_ctx_req_headers_get(HttpCtx* ctx, const char* key) return http_request_get_header(ctx->req, key); } +const char* http_ctx_req_query(HttpCtx* ctx) +{ + return ctx->req->query; +} + const char* http_ctx_req_body(HttpCtx* ctx) { return ctx->req_body; diff --git a/backend/src/main.c b/backend/src/main.c index b8c3175..a9678f7 100644 --- a/backend/src/main.c +++ b/backend/src/main.c @@ -41,6 +41,8 @@ int main(void) http_server_post(server, "/api/carts/purchase", route_post_carts_purchase); + http_server_get(server, "/api/receipts/one", route_get_receipt); + http_server_post(server, "/api/users/register", route_post_users_register); http_server_post(server, "/api/sessions/login", route_post_sessions_login); http_server_post( diff --git a/backend/src/util/str.c b/backend/src/util/str.c index b8054e5..c3fe7cd 100644 --- a/backend/src/util/str.c +++ b/backend/src/util/str.c @@ -14,6 +14,14 @@ char* str_dup(const char* str) return clone; } +const char* str_slice_copy(const StrSlice* slice) +{ + char* copy = malloc(slice->len + 1); + strncpy(copy, slice->ptr, slice->len); + copy[slice->len] = '\0'; + return copy; +} + StrSplitter str_splitter(const char* text, size_t text_len, const char* split) { return (StrSplitter) { @@ -36,10 +44,12 @@ StrSlice str_split_next(StrSplitter* splitter) return (StrSlice) { ptr, len }; } } - return (StrSlice) { + StrSlice slice = { .ptr = &splitter->text[splitter->i], .len = splitter->text_len - splitter->i, }; + splitter->i = splitter->text_len; + return slice; } void string_push_str(String* string, const char* str) diff --git a/backend/src/util/str.h b/backend/src/util/str.h index 4c51e81..576da7f 100644 --- a/backend/src/util/str.h +++ b/backend/src/util/str.h @@ -12,6 +12,8 @@ typedef struct { size_t len; } StrSlice; +const char* str_slice_copy(const StrSlice* slice); + typedef struct { const char* text; size_t text_len; diff --git a/backend/test/test.ts b/backend/test/test.ts index c3c4882..742da7d 100644 --- a/backend/test/test.ts +++ b/backend/test/test.ts @@ -49,7 +49,7 @@ Deno.test("test backend", async (t) => { // console.log(sessionUserRes.user); }); - await testCarts(t, token); + await testCartsAndReceipts(t, token); await t.step("test /api/sessions/logout", async () => { const logoutRes = await post<{ ok: boolean }>( @@ -62,9 +62,11 @@ Deno.test("test backend", async (t) => { }); }); -async function testCarts(t: Deno.TestContext, token: string) { +async function testCartsAndReceipts(t: Deno.TestContext, token: string) { + let receiptId: number | undefined = undefined; + await t.step("test /api/carts/purchase", async () => { - const res = await post<{ ok: boolean }>( + const res = await post<{ ok: boolean; receipt_id: number }>( "/api/carts/purchase", { items: [ @@ -76,6 +78,21 @@ async function testCarts(t: Deno.TestContext, token: string) { ); assertEquals(res.ok, true); + receiptId = res.receipt_id; + }); + + if (!receiptId) { + return; + } + + await t.step("test /api/receipts/one", async () => { + const res = await get<{ ok: boolean }>( + `/api/receipts/one?receipt_id=${receiptId}`, + { "Session-Token": token }, + ); + + console.log(res); + assertEquals(res.ok, true); }); } @@ -83,7 +100,9 @@ function get( path: string, headers: Record, ): Promise { - return fetch(`${url}${path}`, { headers }) + return fetch(`${url}${path}`, { + headers: { ...headers, "Accept": "application/json" }, + }) .then((res) => res.json()); } @@ -94,7 +113,11 @@ function post( ): Promise { return fetch(`${url}${path}`, { method: "post", - headers: { ...headers, "Content-Type": "application/json" }, + headers: { + ...headers, + "Content-Type": "application/json", + "Accept": "application/json", + }, body: JSON.stringify(body), }).then((res) => res.json()); }