get one receipt

This commit is contained in:
SimonFJ20 2025-03-14 16:17:31 +01:00
parent f799db5b32
commit 2d74a872fe
12 changed files with 312 additions and 11 deletions

View File

@ -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, ...) \

View File

@ -1,6 +1,7 @@
#include "../http/http.h"
#include "../models/models_json.h"
#include "controllers.h"
#include <string.h>
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,
"<!DOCTYPE html><html><head><meta "

View File

@ -0,0 +1,107 @@
#include "../models/models_json.h"
#include "../util/str.h"
#include "controllers.h"
#include <stdlib.h>
#include <string.h>
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(&params)).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(&params, "receipt_id");
query_params_destroy(&params);
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);
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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';
}

View File

@ -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);

View File

@ -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;

View File

@ -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(

View File

@ -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)

View File

@ -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;

View File

@ -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<Res>(
path: string,
headers: Record<string, string>,
): Promise<Res> {
return fetch(`${url}${path}`, { headers })
return fetch(`${url}${path}`, {
headers: { ...headers, "Accept": "application/json" },
})
.then((res) => res.json());
}
@ -94,7 +113,11 @@ function post<Res, Req = unknown>(
): Promise<Res> {
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());
}