#include "commands.h" #include "cmd_util.h" #include #include #include #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] };