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_rules.c | |
| parent | beafdbe0d64340387d619c38554976410129f05b (diff) | |
| download | bokf-8351fa7752bb8341ef405a37ff7c73142715e981.tar.gz bokf-8351fa7752bb8341ef405a37ff7c73142715e981.zip | |
commands: split the command table by domain
Diffstat (limited to 'src/cmd_rules.c')
| -rw-r--r-- | src/cmd_rules.c | 398 |
1 files changed, 398 insertions, 0 deletions
diff --git a/src/cmd_rules.c b/src/cmd_rules.c new file mode 100644 index 0000000..04772f3 --- /dev/null +++ b/src/cmd_rules.c @@ -0,0 +1,398 @@ +#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" + +/* ------------------------------------------------------------------ */ +/* report rules (per-org moms mapping) */ +/* ------------------------------------------------------------------ */ + +struct rule_row { + int64_t id; + char report[16]; + char box[8]; + char match_type[16]; + char pattern[64]; + int sign; + int64_t sort_order; +}; + +static int rule_digits(const char *s, size_t min, size_t max) +{ + size_t n = s ? strlen(s) : 0; + if (n < min || n > max) + return 0; + for (const char *p = s; *p; p++) + if (*p < '0' || *p > '9') + return 0; + return 1; +} + +static int rule_validate(const char *box, const char *match_type, + const char *pattern, int64_t sign, char *err, + size_t errlen) +{ + if (!rule_digits(box, 1, 3)) { + snprintf(err, errlen, "box must be 1-3 digits"); + return -1; + } + if (strcmp(match_type, "account") == 0) { + if (!rule_digits(pattern, 1, 10)) { + snprintf(err, errlen, + "pattern must be 1-10 digits when match_type is account"); + return -1; + } + } else if (strcmp(match_type, "range") == 0) { + const char *dash = strchr(pattern, '-'); + if (!dash || dash == pattern || !dash[1] || strchr(dash + 1, '-')) { + snprintf(err, errlen, + "pattern must be LO-HI when match_type is range"); + return -1; + } + char lo[16], hi[16]; + size_t lon = (size_t)(dash - pattern); + size_t hin = strlen(dash + 1); + if (lon >= sizeof lo || hin >= sizeof hi) { + snprintf(err, errlen, "pattern must be LO-HI with digits"); + return -1; + } + memcpy(lo, pattern, lon); + lo[lon] = '\0'; + memcpy(hi, dash + 1, hin + 1); + if (!rule_digits(lo, 1, 10) || !rule_digits(hi, 1, 10)) { + snprintf(err, errlen, + "pattern must be LO-HI with 1-10 digits each"); + return -1; + } + size_t llo = strlen(lo), lhi = strlen(hi); + if (llo > lhi || (llo == lhi && strcmp(lo, hi) > 0)) { + snprintf(err, errlen, "range start must not exceed range end"); + return -1; + } + } else if (strcmp(match_type, "type") == 0) { + if (strcmp(pattern, "asset") != 0 && + strcmp(pattern, "liability") != 0 && + strcmp(pattern, "equity") != 0 && + strcmp(pattern, "revenue") != 0 && + strcmp(pattern, "expense") != 0) { + snprintf(err, errlen, + "pattern must be asset, liability, equity, revenue or" + " expense when match_type is type"); + return -1; + } + } else { + snprintf(err, errlen, "match_type must be account, range or type"); + return -1; + } + if (sign != 1 && sign != -1) { + snprintf(err, errlen, "sign must be 1 or -1"); + return -1; + } + return 0; +} + +static int rule_load(sqlite3 *db, int64_t org_id, int64_t id, + struct rule_row *row) +{ + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + db, + "SELECT id,report,box,match_type,pattern,sign,sort_order" + " FROM report_rules WHERE org_id=?1 AND id=?2", + -1, &st, NULL) != SQLITE_OK) + return -1; + sqlite3_bind_int64(st, 1, org_id); + sqlite3_bind_int64(st, 2, id); + int found = 0; + if (sqlite3_step(st) == SQLITE_ROW) { + memset(row, 0, sizeof *row); + row->id = sqlite3_column_int64(st, 0); + snprintf(row->report, sizeof row->report, "%s", + sq(sqlite3_column_text(st, 1))); + snprintf(row->box, sizeof row->box, "%s", + sq(sqlite3_column_text(st, 2))); + snprintf(row->match_type, sizeof row->match_type, "%s", + sq(sqlite3_column_text(st, 3))); + snprintf(row->pattern, sizeof row->pattern, "%s", + sq(sqlite3_column_text(st, 4))); + row->sign = sqlite3_column_int(st, 5); + row->sort_order = sqlite3_column_int64(st, 6); + found = 1; + } + sqlite3_finalize(st); + return found ? 0 : -1; +} + +static void rule_to_json(yyjson_mut_doc *doc, yyjson_mut_val *o, + const struct rule_row *row) +{ + yyjson_mut_obj_add_int(doc, o, "id", row->id); + yyjson_mut_obj_add_strcpy(doc, o, "report", row->report); + yyjson_mut_obj_add_strcpy(doc, o, "box", row->box); + yyjson_mut_obj_add_strcpy(doc, o, "match_type", row->match_type); + yyjson_mut_obj_add_strcpy(doc, o, "pattern", row->pattern); + yyjson_mut_obj_add_int(doc, o, "sign", row->sign); + yyjson_mut_obj_add_int(doc, o, "sort_order", row->sort_order); +} + +static yyjson_mut_val *h_report_rule_list(struct req *r) +{ + const char *report = arg_str(r->args, "report"); + sqlite3_stmt *st = NULL; + const char *sql = + report ? "SELECT id,report,box,match_type,pattern,sign,sort_order" + " FROM report_rules WHERE org_id=?1 AND report=?2" + " ORDER BY report,sort_order,box,id" + : "SELECT id,report,box,match_type,pattern,sign,sort_order" + " FROM report_rules WHERE org_id=?1" + " ORDER BY report,sort_order,box,id"; + 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 (report) + sqlite3_bind_text(st, 2, report, -1, SQLITE_TRANSIENT); + yyjson_mut_val *items = yyjson_mut_arr(r->rdoc); + while (sqlite3_step(st) == SQLITE_ROW) { + struct rule_row row; + memset(&row, 0, sizeof row); + row.id = sqlite3_column_int64(st, 0); + snprintf(row.report, sizeof row.report, "%s", + sq(sqlite3_column_text(st, 1))); + snprintf(row.box, sizeof row.box, "%s", + sq(sqlite3_column_text(st, 2))); + snprintf(row.match_type, sizeof row.match_type, "%s", + sq(sqlite3_column_text(st, 3))); + snprintf(row.pattern, sizeof row.pattern, "%s", + sq(sqlite3_column_text(st, 4))); + row.sign = sqlite3_column_int(st, 5); + row.sort_order = sqlite3_column_int64(st, 6); + yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items); + rule_to_json(r->rdoc, o, &row); + } + sqlite3_finalize(st); + yyjson_mut_val *res = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_val(r->rdoc, res, "items", items); + return res; +} + +static yyjson_mut_val *h_report_rule_create(struct req *r) +{ + const char *report = arg_str(r->args, "report"); + const char *box = arg_str(r->args, "box"); + const char *match_type = arg_str(r->args, "match_type"); + const char *pattern = arg_str(r->args, "pattern"); + int64_t sign = 1, sort_order = 0; + arg_int(r->args, "sign", &sign); + arg_int(r->args, "sort_order", &sort_order); + if (!report || !box || !match_type || !pattern) + return fail(r, "INVALID_ARGS", + "report, box, match_type and pattern are required"); + if (strcmp(report, "vat") != 0) + return fail(r, "INVALID_ARGS", "report must be \"vat\""); + char verr[256]; + if (rule_validate(box, match_type, pattern, sign, verr, sizeof verr) != 0) + return fail(r, "INVALID_ARGS", verr); + if (r->dry_run) { + struct rule_row row; + memset(&row, 0, sizeof row); + snprintf(row.report, sizeof row.report, "%s", report); + snprintf(row.box, sizeof row.box, "%s", box); + snprintf(row.match_type, sizeof row.match_type, "%s", match_type); + snprintf(row.pattern, sizeof row.pattern, "%s", pattern); + row.sign = (int)sign; + row.sort_order = sort_order; + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + rule_to_json(r->rdoc, o, &row); + yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); + return o; + } + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "INSERT INTO report_rules(org_id,report,box,match_type,pattern," + "sign,sort_order) 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, report, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 3, box, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 4, match_type, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 5, pattern, -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(st, 6, sign); + sqlite3_bind_int64(st, 7, sort_order); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + 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, + "report_rule.create", reqjson, "OK", NULL); + free(reqjson); + struct rule_row row; + memset(&row, 0, sizeof row); + row.id = id; + snprintf(row.report, sizeof row.report, "%s", report); + snprintf(row.box, sizeof row.box, "%s", box); + snprintf(row.match_type, sizeof row.match_type, "%s", match_type); + snprintf(row.pattern, sizeof row.pattern, "%s", pattern); + row.sign = (int)sign; + row.sort_order = sort_order; + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + rule_to_json(r->rdoc, o, &row); + return o; +} + +static yyjson_mut_val *h_report_rule_update(struct req *r) +{ + int64_t id = 0; + arg_int(r->args, "id", &id); + if (id <= 0) + return fail(r, "INVALID_ARGS", "id is required"); + struct rule_row row; + if (rule_load(r->db, r->org_id, id, &row) != 0) + return fail(r, "NOT_FOUND", "report rule not found"); + const char *box = arg_str(r->args, "box"); + const char *match_type = arg_str(r->args, "match_type"); + const char *pattern = arg_str(r->args, "pattern"); + int64_t sign = row.sign, sort_order = row.sort_order; + arg_int(r->args, "sign", &sign); + arg_int(r->args, "sort_order", &sort_order); + if (box) + snprintf(row.box, sizeof row.box, "%s", box); + if (match_type) + snprintf(row.match_type, sizeof row.match_type, "%s", match_type); + if (pattern) + snprintf(row.pattern, sizeof row.pattern, "%s", pattern); + row.sign = (int)sign; + row.sort_order = sort_order; + char verr[256]; + if (rule_validate(row.box, row.match_type, row.pattern, row.sign, verr, + sizeof verr) != 0) + return fail(r, "INVALID_ARGS", verr); + if (r->dry_run) { + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + rule_to_json(r->rdoc, o, &row); + yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); + return o; + } + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "UPDATE report_rules SET box=?3,match_type=?4,pattern=?5,sign=?6," + "sort_order=?7 WHERE org_id=?1 AND id=?2", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, row.id); + sqlite3_bind_text(st, 3, row.box, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 4, row.match_type, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 5, row.pattern, -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(st, 6, row.sign); + sqlite3_bind_int64(st, 7, row.sort_order); + 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", "report rule not found"); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "report_rule.update", reqjson, "OK", NULL); + free(reqjson); + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + rule_to_json(r->rdoc, o, &row); + return o; +} + +static yyjson_mut_val *h_report_rule_delete(struct req *r) +{ + int64_t id = 0; + arg_int(r->args, "id", &id); + if (id <= 0) + return fail(r, "INVALID_ARGS", "id is required"); + struct rule_row row; + if (rule_load(r->db, r->org_id, id, &row) != 0) + return fail(r, "NOT_FOUND", "report rule not found"); + if (r->dry_run) { + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_int(r->rdoc, o, "id", id); + yyjson_mut_obj_add_bool(r->rdoc, o, "deleted", true); + yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); + return o; + } + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, "DELETE FROM report_rules WHERE org_id=?1 AND id=?2", -1, + &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, 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", "report rule not found"); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "report_rule.delete", 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_bool(r->rdoc, o, "deleted", true); + return o; +} + + +static const struct cmd_arg args_report_rule_list[] = { + { "report", ARG_ENUM, 0, NULL, "vat", + "Report filter; omit for all reports" }, +}; + +static const struct cmd_arg args_report_rule_create[] = { + { "report", ARG_ENUM, 1, NULL, "vat", "Report the rule belongs to" }, + { "box", ARG_STR, 1, NULL, NULL, "Blankett box, 1-3 digits" }, + { "match_type", ARG_ENUM, 1, NULL, "account,range,type", + "How pattern selects accounts" }, + { "pattern", ARG_STR, 1, NULL, NULL, + "Account, account range or account type" }, + { "sign", ARG_INT, 0, "1", NULL, "1 or -1" }, + { "sort_order", ARG_INT, 0, "0", NULL, "Evaluation order" }, +}; + +static const struct cmd_arg args_report_rule_update[] = { + { "id", ARG_INT, 1, NULL, NULL, "Rule id" }, + { "box", ARG_STR, 0, NULL, NULL, "Blankett box, 1-3 digits" }, + { "match_type", ARG_ENUM, 0, NULL, "account,range,type", + "How pattern selects accounts" }, + { "pattern", ARG_STR, 0, NULL, NULL, + "Account, account range or account type" }, + { "sign", ARG_INT, 0, NULL, NULL, "1 or -1" }, + { "sort_order", ARG_INT, 0, NULL, NULL, "Evaluation order" }, +}; + +static const struct cmd_arg args_report_rule_delete[] = { + { "id", ARG_INT, 1, NULL, NULL, "Rule id" }, +}; + +const struct command g_cmd_rules[] = { + { "report.rule_list", "List per-org reporting rules", PERM_READ, 1, 0, 0, + h_report_rule_list, CMD_ARGS(args_report_rule_list) }, + { "report.rule_create", "Create a reporting rule (owner)", PERM_OWNER, 1, 1, + 1, h_report_rule_create, CMD_ARGS(args_report_rule_create) }, + { "report.rule_update", "Update a reporting rule (owner)", PERM_OWNER, 1, 1, + 1, h_report_rule_update, CMD_ARGS(args_report_rule_update) }, + { "report.rule_delete", "Delete a reporting rule (owner)", PERM_OWNER, 1, 1, + 1, h_report_rule_delete, CMD_ARGS(args_report_rule_delete) }, +}; + +const struct cmd_table g_cmd_table_rules = { + g_cmd_rules, sizeof g_cmd_rules / sizeof g_cmd_rules[0] +}; |
