add carts purchase

This commit is contained in:
SimonFJ20 2025-03-14 13:39:53 +01:00
parent de4e91db72
commit c49f0c05d2
9 changed files with 384 additions and 34 deletions

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
#include "../collections/collection.h"
#include "../models/models.h"
#include <stdbool.h>
#include <stdint.h>
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);

View File

@ -7,6 +7,13 @@
#include <stdint.h>
#include <stdio.h>
#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, &current_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;
}

View File

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

View File

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

View File

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

View File

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