#include "commands.h" #include "cmd_util.h" #include #include #include #include "audit.h" #include "db.h" #include "formula.h" #include "util.h" /* ------------------------------------------------------------------ */ /* voucher templates (konteringsmallar) */ /* ------------------------------------------------------------------ */ struct tpl_row_in { int64_t account_id; char account[16]; char formula[256]; char desc[128]; }; static int parse_template_rows(struct req *r, yyjson_val *rowsv, struct tpl_row_in **out, size_t *out_n) { *out = NULL; *out_n = 0; if (!rowsv || !yyjson_is_arr(rowsv)) return fail(r, "INVALID_ARGS", "rows must be an array") ? -1 : -1; size_t n = yyjson_arr_size(rowsv); if (n == 0 || n > 100) return fail(r, "INVALID_ARGS", "a template needs between 1 and 100 rows") ? -1 : -1; struct tpl_row_in *rows = xcalloc(n, sizeof *rows); size_t k = 0; yyjson_arr_iter it = yyjson_arr_iter_with(rowsv); yyjson_val *item; while ((item = yyjson_arr_iter_next(&it))) { if (!yyjson_is_obj(item)) { free(rows); fail(r, "INVALID_ARGS", "each row must be an object"); return -1; } yyjson_val *av = yyjson_obj_get(item, "account"); yyjson_val *fv = yyjson_obj_get(item, "formula"); if (!av || !yyjson_is_str(av) || !fv || !yyjson_is_str(fv)) { free(rows); failf(r, "INVALID_ARGS", "row %zu: account and formula are required", k + 1); return -1; } const char *account = yyjson_get_str(av); const char *formula = yyjson_get_str(fv); if (!formula_valid(formula)) { free(rows); failf(r, "INVALID_ARGS", "row %zu: ogiltig formel '%s'", k + 1, formula); return -1; } sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2(r->db, "SELECT id FROM accounts WHERE org_id=?1" " AND number=?2", -1, &st, NULL) != SQLITE_OK) { free(rows); return fail(r, "INTERNAL", "database error") ? -1 : -1; } sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_text(st, 2, account, -1, SQLITE_TRANSIENT); if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); free(rows); failf(r, "ACCOUNT_NOT_FOUND", "row %zu: account %s does not exist", k + 1, account); return -1; } rows[k].account_id = sqlite3_column_int64(st, 0); sqlite3_finalize(st); snprintf(rows[k].account, sizeof rows[k].account, "%s", account); snprintf(rows[k].formula, sizeof rows[k].formula, "%s", formula); yyjson_val *dv = yyjson_obj_get(item, "description"); if (dv && yyjson_is_str(dv)) snprintf(rows[k].desc, sizeof rows[k].desc, "%s", yyjson_get_str(dv)); k++; } *out = rows; *out_n = k; return 0; } static yyjson_mut_val *tpl_rows_json(struct req *r, sqlite3_stmt *st) { yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_int(r->rdoc, o, "line_no", sqlite3_column_int64(st, 0)); yyjson_mut_obj_add_strcpy(r->rdoc, o, "account", sq(sqlite3_column_text(st, 1))); yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", sq(sqlite3_column_text(st, 2))); yyjson_mut_obj_add_strcpy(r->rdoc, o, "formula", sq(sqlite3_column_text(st, 3))); yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", sq(sqlite3_column_text(st, 4))); return o; } static yyjson_mut_val *h_template_list(struct req *r) { int active_only = 0; arg_bool(r->args, "active_only", &active_only); yyjson_mut_val *items = yyjson_mut_arr(r->rdoc); sqlite3_stmt *st = NULL; const char *sql = active_only ? "SELECT t.id,t.name,t.series,t.description,t.active," "(SELECT count(*) FROM voucher_template_rows tr" " WHERE tr.org_id=t.org_id AND tr.template_id=t.id)" " FROM voucher_templates t WHERE t.org_id=?1 AND t.active=1" " ORDER BY t.name" : "SELECT t.id,t.name,t.series,t.description,t.active," "(SELECT count(*) FROM voucher_template_rows tr" " WHERE tr.org_id=t.org_id AND tr.template_id=t.id)" " FROM voucher_templates t WHERE t.org_id=?1 ORDER BY t.name"; if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK) return fail(r, "INTERNAL", "database error"); sqlite3_bind_int64(st, 1, r->org_id); while (sqlite3_step(st) == SQLITE_ROW) { yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items); yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0)); yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", sq(sqlite3_column_text(st, 1))); yyjson_mut_obj_add_strcpy(r->rdoc, o, "series", sq(sqlite3_column_text(st, 2))); yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", sq(sqlite3_column_text(st, 3))); yyjson_mut_obj_add_bool(r->rdoc, o, "active", sqlite3_column_int(st, 4) != 0); yyjson_mut_obj_add_int(r->rdoc, o, "row_count", sqlite3_column_int64(st, 5)); } sqlite3_finalize(st); yyjson_mut_val *out = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_val(r->rdoc, out, "items", items); return out; } static yyjson_mut_val *h_template_get(struct req *r) { int64_t id = 0; arg_int(r->args, "id", &id); const char *name = arg_str(r->args, "name"); if (id <= 0 && !name) return fail(r, "INVALID_ARGS", "id or name is required"); sqlite3_stmt *st = NULL; const char *sql = id > 0 ? "SELECT id,name,series,description,active" " FROM voucher_templates WHERE org_id=?1 AND id=?2" : "SELECT id,name,series,description,active" " FROM voucher_templates WHERE org_id=?1 AND name=?2"; if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK) return fail(r, "INTERNAL", "database error"); sqlite3_bind_int64(st, 1, r->org_id); if (id > 0) sqlite3_bind_int64(st, 2, id); else sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT); if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return fail(r, "NOT_FOUND", "template not found"); } int64_t tpl = sqlite3_column_int64(st, 0); yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_int(r->rdoc, o, "id", tpl); yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", sq(sqlite3_column_text(st, 1))); yyjson_mut_obj_add_strcpy(r->rdoc, o, "series", sq(sqlite3_column_text(st, 2))); yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", sq(sqlite3_column_text(st, 3))); yyjson_mut_obj_add_bool(r->rdoc, o, "active", sqlite3_column_int(st, 4) != 0); sqlite3_finalize(st); yyjson_mut_val *rows = yyjson_mut_arr(r->rdoc); if (sqlite3_prepare_v2( r->db, "SELECT tr.line_no,a.number,a.name,tr.formula," "COALESCE(tr.description,'') FROM voucher_template_rows tr" " JOIN accounts a ON a.org_id=tr.org_id AND a.id=tr.account_id" " WHERE tr.org_id=?1 AND tr.template_id=?2 ORDER BY tr.line_no", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_int64(st, 2, tpl); while (sqlite3_step(st) == SQLITE_ROW) yyjson_mut_arr_add_val(rows, tpl_rows_json(r, st)); sqlite3_finalize(st); } yyjson_mut_obj_add_val(r->rdoc, o, "rows", rows); return o; } static yyjson_mut_val *h_template_create(struct req *r) { const char *name = arg_str(r->args, "name"); const char *series = arg_str(r->args, "series"); const char *description = arg_str(r->args, "description"); if (!name || !*name) return fail(r, "INVALID_ARGS", "name is required"); char *series_owned = NULL; if (!series || !*series) { series_owned = db_setting(r->db, r->org_id, "default_series"); series = series_owned && *series_owned ? series_owned : "A"; } if (!description) description = ""; struct tpl_row_in *rows = NULL; size_t nrows = 0; if (parse_template_rows( r, r->args ? yyjson_obj_get(r->args, "rows") : NULL, &rows, &nrows) != 0) return NULL; if (r->dry_run) { yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name); yyjson_mut_obj_add_strcpy(r->rdoc, o, "series", series); yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", description); yyjson_mut_val *arr = yyjson_mut_arr(r->rdoc); for (size_t i = 0; i < nrows; i++) { yyjson_mut_val *ro = yyjson_mut_arr_add_obj(r->rdoc, arr); yyjson_mut_obj_add_strcpy(r->rdoc, ro, "account", rows[i].account); yyjson_mut_obj_add_strcpy(r->rdoc, ro, "formula", rows[i].formula); yyjson_mut_obj_add_strcpy(r->rdoc, ro, "description", rows[i].desc); } yyjson_mut_obj_add_val(r->rdoc, o, "rows", arr); yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); free(rows); free(series_owned); return o; } char ts[32]; util_iso8601(util_now(), ts, sizeof ts); if (db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0) { free(rows); free(series_owned); return fail(r, "DB_BUSY", "could not start transaction"); } sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2(r->db, "INSERT INTO voucher_templates(org_id,name,series," "description,created_at) VALUES(?1,?2,?3,?4,?5)", -1, &st, NULL) != SQLITE_OK) { db_exec(r->db, "ROLLBACK", NULL); free(rows); return fail(r, "INTERNAL", "database error"); } sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, series, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 4, description, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 5, ts, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) { db_exec(r->db, "ROLLBACK", NULL); free(rows); if ((rc & 0xff) == SQLITE_CONSTRAINT) return fail(r, "CONFLICT", "a template with that name exists"); return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); } int64_t tpl = db_last_id(r->db); for (size_t i = 0; i < nrows; i++) { if (sqlite3_prepare_v2( r->db, "INSERT INTO voucher_template_rows(org_id,template_id,line_no," "account_id,formula,description) VALUES(?1,?2,?3,?4,?5,?6)", -1, &st, NULL) != SQLITE_OK) { db_exec(r->db, "ROLLBACK", NULL); free(rows); return fail(r, "INTERNAL", "database error"); } sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_int64(st, 2, tpl); sqlite3_bind_int64(st, 3, (int64_t)i + 1); sqlite3_bind_int64(st, 4, rows[i].account_id); sqlite3_bind_text(st, 5, rows[i].formula, -1, SQLITE_TRANSIENT); if (rows[i].desc[0]) sqlite3_bind_text(st, 6, rows[i].desc, -1, SQLITE_TRANSIENT); else sqlite3_bind_null(st, 6); rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) { db_exec(r->db, "ROLLBACK", NULL); free(rows); return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); } } free(rows); if (db_exec(r->db, "COMMIT", NULL) != 0) return fail(r, "INTERNAL", "commit failed"); char *reqjson = audit_args_json(r->args); audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, "template.create", reqjson, "OK", NULL); free(reqjson); yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); yyjson_mut_obj_add_int(r->rdoc, o, "id", tpl); yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name); yyjson_mut_obj_add_strcpy(r->rdoc, o, "series", series); yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", description); free(series_owned); return o; } static yyjson_mut_val *h_template_update(struct req *r) { int64_t id = 0; arg_int(r->args, "id", &id); const char *name_arg = arg_str(r->args, "name"); if (id <= 0 && !name_arg) return fail(r, "INVALID_ARGS", "id or name is required"); struct tpl_head head; char *err = NULL; if (load_template(r->db, r->org_id, id, name_arg, &head, &err) != 0) { yyjson_mut_val *res = fail(r, "NOT_FOUND", err ? err : "template not found"); free(err); return res; } const char *new_name = arg_str(r->args, "name"); const char *series = arg_str(r->args, "series"); const char *description = arg_str(r->args, "description"); int active = -1; arg_bool(r->args, "active", &active); yyjson_val *rowsv = r->args ? yyjson_obj_get(r->args, "rows") : NULL; struct tpl_row_in *rows = NULL; size_t nrows = 0; if (rowsv) { if (parse_template_rows(r, rowsv, &rows, &nrows) != 0) return NULL; } if (!new_name && !series && !description && active < 0 && !rowsv) { free(rows); return fail(r, "INVALID_ARGS", "nothing to update"); } if (r->dry_run) { free(rows); return h_template_get(r); } char ts[32]; util_iso8601(util_now(), ts, sizeof ts); if (db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0) { free(rows); return fail(r, "DB_BUSY", "could not start transaction"); } sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2( r->db, "UPDATE voucher_templates SET" " name=COALESCE(?2,name), series=COALESCE(?3,series)," " description=COALESCE(?4,description)," " active=CASE WHEN ?5<0 THEN active ELSE ?5 END, updated_at=?6" " WHERE org_id=?1 AND id=?7", -1, &st, NULL) != SQLITE_OK) { db_exec(r->db, "ROLLBACK", NULL); free(rows); return fail(r, "INTERNAL", "database error"); } sqlite3_bind_int64(st, 1, r->org_id); if (new_name) sqlite3_bind_text(st, 2, new_name, -1, SQLITE_TRANSIENT); else sqlite3_bind_null(st, 2); if (series) sqlite3_bind_text(st, 3, series, -1, SQLITE_TRANSIENT); else sqlite3_bind_null(st, 3); if (description) sqlite3_bind_text(st, 4, description, -1, SQLITE_TRANSIENT); else sqlite3_bind_null(st, 4); sqlite3_bind_int(st, 5, active); sqlite3_bind_text(st, 6, ts, -1, SQLITE_TRANSIENT); sqlite3_bind_int64(st, 7, head.id); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) { db_exec(r->db, "ROLLBACK", NULL); free(rows); return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); } if (rowsv) { if (sqlite3_prepare_v2( r->db, "DELETE FROM voucher_template_rows WHERE org_id=?1" " AND template_id=?2", -1, &st, NULL) != SQLITE_OK) { db_exec(r->db, "ROLLBACK", NULL); free(rows); return fail(r, "INTERNAL", "database error"); } sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_int64(st, 2, head.id); rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) { db_exec(r->db, "ROLLBACK", NULL); free(rows); return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); } for (size_t i = 0; i < nrows; i++) { if (sqlite3_prepare_v2( r->db, "INSERT INTO voucher_template_rows(org_id,template_id," "line_no,account_id,formula,description)" " VALUES(?1,?2,?3,?4,?5,?6)", -1, &st, NULL) != SQLITE_OK) { db_exec(r->db, "ROLLBACK", NULL); free(rows); return fail(r, "INTERNAL", "database error"); } sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_int64(st, 2, head.id); sqlite3_bind_int64(st, 3, (int64_t)i + 1); sqlite3_bind_int64(st, 4, rows[i].account_id); sqlite3_bind_text(st, 5, rows[i].formula, -1, SQLITE_TRANSIENT); if (rows[i].desc[0]) sqlite3_bind_text(st, 6, rows[i].desc, -1, SQLITE_TRANSIENT); else sqlite3_bind_null(st, 6); rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) { db_exec(r->db, "ROLLBACK", NULL); free(rows); return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); } } } free(rows); if (db_exec(r->db, "COMMIT", NULL) != 0) return fail(r, "INTERNAL", "commit failed"); char *reqjson = audit_args_json(r->args); audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, "template.update", reqjson, "OK", NULL); free(reqjson); return h_template_get(r); } static yyjson_mut_val *h_template_archive(struct req *r) { int64_t id = 0; arg_int(r->args, "id", &id); const char *name = arg_str(r->args, "name"); if (id <= 0 && !name) return fail(r, "INVALID_ARGS", "id or name is required"); sqlite3_stmt *st = NULL; const char *sql = id > 0 ? "UPDATE voucher_templates SET active=0 WHERE org_id=?1" " AND id=?2" : "UPDATE voucher_templates SET active=0 WHERE org_id=?1" " AND name=?2"; if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK) return fail(r, "INTERNAL", "database error"); sqlite3_bind_int64(st, 1, r->org_id); if (id > 0) sqlite3_bind_int64(st, 2, id); else sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); if (sqlite3_changes(r->db) == 0) return fail(r, "NOT_FOUND", "template not found"); char *reqjson = audit_args_json(r->args); audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, "template.archive", reqjson, "OK", NULL); free(reqjson); return yyjson_mut_obj(r->rdoc); } static const struct cmd_arg args_template_list[] = { { "active_only", ARG_BOOL, 0, NULL, NULL, "Only active templates" }, }; static const struct cmd_arg args_template_get[] = { { "id", ARG_INT, 0, NULL, NULL, "Template id" }, { "name", ARG_STR, 0, NULL, NULL, "Template name" }, }; static const struct cmd_arg args_template_create[] = { { "name", ARG_STR, 1, NULL, NULL, "Template name" }, { "series", ARG_STR, 0, NULL, NULL, "Number series; defaults to settings.default_series" }, { "description", ARG_STR, 0, NULL, NULL, "Voucher text; {x} is replaced with the amount" }, { "rows", ARG_JSON, 1, NULL, NULL, "Array of {account,formula,description?}" }, }; static const struct cmd_arg args_template_update[] = { { "id", ARG_INT, 0, NULL, NULL, "Template id" }, { "name", ARG_STR, 0, NULL, NULL, "Template name, or the new name when id is given" }, { "series", ARG_STR, 0, NULL, NULL, "Number series" }, { "description", ARG_STR, 0, NULL, NULL, "Voucher text" }, { "active", ARG_BOOL, 0, NULL, NULL, "Active flag" }, { "rows", ARG_JSON, 0, NULL, NULL, "Replacement rows, {account,formula,description?}" }, }; static const struct cmd_arg args_template_archive[] = { { "id", ARG_INT, 0, NULL, NULL, "Template id" }, { "name", ARG_STR, 0, NULL, NULL, "Template name" }, }; const struct command g_cmd_templates[] = { { "template.list", "List voucher templates", PERM_READ, 1, 0, 0, h_template_list, CMD_ARGS(args_template_list) }, { "template.get", "Get a voucher template with its rows", PERM_READ, 1, 0, 0, h_template_get, CMD_ARGS(args_template_get) }, { "template.create", "Create a voucher template", PERM_WRITE, 1, 1, 1, h_template_create, CMD_ARGS(args_template_create) }, { "template.update", "Update a voucher template", PERM_WRITE, 1, 1, 1, h_template_update, CMD_ARGS(args_template_update) }, { "template.archive", "Archive a voucher template", PERM_WRITE, 1, 1, 1, h_template_archive, CMD_ARGS(args_template_archive) }, }; const struct cmd_table g_cmd_table_templates = { g_cmd_templates, sizeof g_cmd_templates / sizeof g_cmd_templates[0] };