#include "commands.h" #include "cmd_util.h" #include #include #include #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 db_error(r); 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 db_error(r); 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 db_sqlite_error(r); 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 db_error(r); 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 db_sqlite_error(r); 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; } if (req_exec(r, "DELETE FROM report_rules WHERE org_id=?1 AND id=?2", "ii", r->org_id, id) != 0) return NULL; 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] };