diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-20 20:11:19 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-20 20:11:19 +0200 |
| commit | 8351fa7752bb8341ef405a37ff7c73142715e981 (patch) | |
| tree | dde603d313f4f26934f2ce20a22dfd9b69324cf0 /src/cmd_accounts.c | |
| parent | beafdbe0d64340387d619c38554976410129f05b (diff) | |
| download | bokf-8351fa7752bb8341ef405a37ff7c73142715e981.tar.gz bokf-8351fa7752bb8341ef405a37ff7c73142715e981.zip | |
commands: split the command table by domain
Diffstat (limited to 'src/cmd_accounts.c')
| -rw-r--r-- | src/cmd_accounts.c | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/src/cmd_accounts.c b/src/cmd_accounts.c new file mode 100644 index 0000000..900558f --- /dev/null +++ b/src/cmd_accounts.c @@ -0,0 +1,246 @@ +#include "commands.h" +#include "cmd_util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "audit.h" +#include "db.h" +#include "util.h" + +/* ------------------------------------------------------------------ */ +/* kontoplan */ +/* ------------------------------------------------------------------ */ + +static yyjson_mut_val *account_json(yyjson_mut_doc *doc, sqlite3_stmt *st) +{ + yyjson_mut_val *o = yyjson_mut_obj(doc); + yyjson_mut_obj_add_int(doc, o, "id", sqlite3_column_int64(st, 0)); + yyjson_mut_obj_add_strcpy(doc, o, "number", + sq(sqlite3_column_text(st, 1))); + yyjson_mut_obj_add_strcpy(doc, o, "name", sq(sqlite3_column_text(st, 2))); + yyjson_mut_obj_add_strcpy(doc, o, "type", sq(sqlite3_column_text(st, 3))); + if (sqlite3_column_type(st, 4) == SQLITE_NULL) + yyjson_mut_obj_add_null(doc, o, "sru_code"); + else + yyjson_mut_obj_add_strcpy(doc, o, "sru_code", + sq(sqlite3_column_text(st, 4))); + if (sqlite3_column_type(st, 5) == SQLITE_NULL) + yyjson_mut_obj_add_null(doc, o, "vat_code"); + else + yyjson_mut_obj_add_strcpy(doc, o, "vat_code", + sq(sqlite3_column_text(st, 5))); + yyjson_mut_obj_add_bool(doc, o, "active", sqlite3_column_int(st, 6) != 0); + return o; +} + +#define ACCOUNT_COLUMNS "id,number,name,type,sru_code,vat_code,active" + +static yyjson_mut_val *h_account_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 " ACCOUNT_COLUMNS " FROM accounts" + " WHERE org_id=?1 AND active=1 ORDER BY number" + : "SELECT " ACCOUNT_COLUMNS " FROM accounts" + " WHERE org_id=?1 ORDER BY number"; + 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_arr_add_val(items, account_json(r->rdoc, st)); + sqlite3_finalize(st); + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_val(r->rdoc, o, "items", items); + return o; +} + +static yyjson_mut_val *h_account_get(struct req *r) +{ + int64_t id = 0; + const char *number = arg_str(r->args, "number"); + arg_int(r->args, "id", &id); + if (!id && !number) + return fail(r, "INVALID_ARGS", "id or number is required"); + sqlite3_stmt *st = NULL; + const char *sql = id ? "SELECT " ACCOUNT_COLUMNS " FROM accounts" + " WHERE org_id=?1 AND id=?2" + : "SELECT " ACCOUNT_COLUMNS " FROM accounts" + " WHERE org_id=?1 AND number=?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) + sqlite3_bind_int64(st, 2, id); + else + sqlite3_bind_text(st, 2, number, -1, SQLITE_TRANSIENT); + yyjson_mut_val *o = NULL; + if (sqlite3_step(st) == SQLITE_ROW) + o = account_json(r->rdoc, st); + sqlite3_finalize(st); + if (!o) + return fail(r, "NOT_FOUND", "account not found"); + return o; +} + +static int valid_account_type(const char *t) +{ + return t && (!strcmp(t, "asset") || !strcmp(t, "liability") || + !strcmp(t, "equity") || !strcmp(t, "revenue") || + !strcmp(t, "expense")); +} + +static yyjson_mut_val *h_account_create(struct req *r) +{ + const char *number = arg_str(r->args, "number"); + const char *name = arg_str(r->args, "name"); + const char *type = arg_str(r->args, "type"); + const char *sru = arg_str(r->args, "sru_code"); + const char *vat = arg_str(r->args, "vat_code"); + if (!is_digits(number) || strlen(number) > 10) + return fail(r, "INVALID_ARGS", "number must be 1-10 digits"); + if (!name || !*name) + return fail(r, "INVALID_ARGS", "name is required"); + if (!valid_account_type(type)) + return fail(r, "INVALID_ARGS", + "type must be asset, liability, equity, revenue or expense"); + char ts[32]; + util_iso8601(util_now(), ts, sizeof ts); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "INSERT INTO accounts(org_id,number,name,type,sru_code,vat_code," + "created_at) VALUES(?1,?2,?3,?4,?5,?6,?7)", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_text(st, 2, number, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 3, name, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 4, type, -1, SQLITE_TRANSIENT); + if (sru) + sqlite3_bind_text(st, 5, sru, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, 5); + if (vat) + sqlite3_bind_text(st, 6, vat, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, 6); + sqlite3_bind_text(st, 7, ts, -1, SQLITE_TRANSIENT); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) { + if ((rc & 0xff) == SQLITE_CONSTRAINT) + return fail(r, "CONFLICT", "account number already exists"); + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + } + int64_t id = db_last_id(r->db); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "account.create", reqjson, "OK", NULL); + free(reqjson); + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_int(r->rdoc, o, "id", id); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "number", number); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "type", type); + return o; +} + +static yyjson_mut_val *h_account_update(struct req *r) +{ + int64_t id = 0; + if (!arg_int(r->args, "id", &id) || id <= 0) + return fail(r, "INVALID_ARGS", "id is required"); + const char *name = arg_str(r->args, "name"); + const char *sru = arg_str(r->args, "sru_code"); + const char *vat = arg_str(r->args, "vat_code"); + int active = -1; + arg_bool(r->args, "active", &active); + if (!name && !sru && !vat && active < 0) + return fail(r, "INVALID_ARGS", "nothing to update"); + char ts[32]; + util_iso8601(util_now(), ts, sizeof ts); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "UPDATE accounts SET" + " name=COALESCE(?2,name), sru_code=COALESCE(?3,sru_code)," + " vat_code=COALESCE(?4,vat_code)," + " 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) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + if (name) + sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, 2); + if (sru) + sqlite3_bind_text(st, 3, sru, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, 3); + if (vat) + sqlite3_bind_text(st, 4, vat, -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, id); + 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", "account not found"); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "account.update", reqjson, "OK", NULL); + free(reqjson); + return yyjson_mut_obj(r->rdoc); +} + + +static const struct cmd_arg args_account_list[] = { + { "active_only", ARG_BOOL, 0, NULL, NULL, "Only active accounts" }, +}; + +static const struct cmd_arg args_account_get[] = { + { "id", ARG_INT, 0, NULL, NULL, "Account id" }, + { "number", ARG_STR, 0, NULL, NULL, "Account number (string)" }, +}; + +static const struct cmd_arg args_account_create[] = { + { "number", ARG_STR, 1, NULL, NULL, "Account number, 1-10 digits" }, + { "name", ARG_STR, 1, NULL, NULL, "Account name" }, + { "type", ARG_ENUM, 1, NULL, "asset,liability,equity,revenue,expense", + "Account type" }, + { "sru_code", ARG_STR, 0, NULL, NULL, "SRU code" }, + { "vat_code", ARG_STR, 0, NULL, NULL, "VAT code" }, +}; + +static const struct cmd_arg args_account_update[] = { + { "id", ARG_INT, 1, NULL, NULL, "Account id" }, + { "name", ARG_STR, 0, NULL, NULL, "Account name" }, + { "sru_code", ARG_STR, 0, NULL, NULL, "SRU code" }, + { "vat_code", ARG_STR, 0, NULL, NULL, "VAT code" }, + { "active", ARG_BOOL, 0, NULL, NULL, "Active flag" }, +}; + +const struct command g_cmd_accounts[] = { + { "account.list", "List the chart of accounts", PERM_READ, 1, 0, 0, + h_account_list, CMD_ARGS(args_account_list) }, + { "account.get", "Get one account", PERM_READ, 1, 0, 0, h_account_get, + CMD_ARGS(args_account_get) }, + { "account.create", "Add an account", PERM_WRITE, 1, 1, 1, + h_account_create, CMD_ARGS(args_account_create) }, + { "account.update", "Rename or deactivate an account", PERM_WRITE, 1, 1, 1, + h_account_update, CMD_ARGS(args_account_update) }, +}; + +const struct cmd_table g_cmd_table_accounts = { + g_cmd_accounts, sizeof g_cmd_accounts / sizeof g_cmd_accounts[0] +}; |
