summaryrefslogtreecommitdiff
path: root/src/cmd_accounts.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd_accounts.c')
-rw-r--r--src/cmd_accounts.c246
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]
+};