#include "cmd_util.h" #include #include #include #include #include #include #include #include "db.h" #include "util.h" /* ------------------------------------------------------------------ */ /* small helpers */ /* ------------------------------------------------------------------ */ const char *sq(const unsigned char *p) { return p ? (const char *)p : ""; } const char *arg_str(yyjson_val *args, const char *key) { if (!args || !yyjson_is_obj(args)) return NULL; yyjson_val *v = yyjson_obj_get(args, key); return v && yyjson_is_str(v) ? yyjson_get_str(v) : NULL; } int arg_int(yyjson_val *args, const char *key, int64_t *out) { if (!args || !yyjson_is_obj(args)) return 0; yyjson_val *v = yyjson_obj_get(args, key); if (!v || !yyjson_is_int(v)) return 0; *out = yyjson_get_int(v); return 1; } int arg_bool(yyjson_val *args, const char *key, int *out) { if (!args || !yyjson_is_obj(args)) return 0; yyjson_val *v = yyjson_obj_get(args, key); if (!v || !yyjson_is_bool(v)) return 0; *out = yyjson_get_bool(v) ? 1 : 0; return 1; } const char *arg_type_name(enum arg_type type) { switch (type) { case ARG_STR: return "string"; case ARG_INT: return "int"; case ARG_BOOL: return "bool"; case ARG_ENUM: return "enum"; case ARG_DATE: return "date"; case ARG_JSON: return "json"; } return "json"; } int enum_allowed(const char *values, const char *v) { size_t n = strlen(v); const char *p = values; while (p && *p) { const char *comma = strchr(p, ','); size_t len = comma ? (size_t)(comma - p) : strlen(p); if (len == n && strncmp(p, v, n) == 0) return 1; if (!comma) break; p = comma + 1; } return 0; } void arg_error(char *err, size_t errlen, const char *fmt, ...) __attribute__((format(printf, 3, 4))); void arg_error(char *err, size_t errlen, const char *fmt, ...) { if (!err || errlen == 0) return; va_list ap; va_start(ap, fmt); vsnprintf(err, errlen, fmt, ap); va_end(ap); } int command_validate_args(const struct command *cmd, yyjson_val *args, char *err, size_t errlen) { if (!cmd || !cmd->args) return 0; if (err && errlen) err[0] = '\0'; for (size_t i = 0; i < cmd->nargs; i++) { const struct cmd_arg *a = &cmd->args[i]; yyjson_val *v = args && yyjson_is_obj(args) ? yyjson_obj_get(args, a->name) : NULL; if (!v || yyjson_is_null(v)) { if (a->required) { arg_error(err, errlen, "missing required argument \"%s\"", a->name); return -1; } continue; } int bad = 0, empty = 0; switch (a->type) { case ARG_STR: bad = !yyjson_is_str(v); empty = !bad && a->required && !*yyjson_get_str(v); break; case ARG_INT: bad = !yyjson_is_int(v); break; case ARG_BOOL: bad = !yyjson_is_bool(v); break; case ARG_ENUM: bad = !yyjson_is_str(v) || !enum_allowed(a->values, yyjson_get_str(v)); empty = !bad && a->required && !*yyjson_get_str(v); break; case ARG_DATE: bad = !yyjson_is_str(v) || !util_parse_iso_date(yyjson_get_str(v)); break; case ARG_JSON: break; } if (empty) { arg_error(err, errlen, "missing required argument \"%s\"", a->name); return -1; } if (bad) { switch (a->type) { case ARG_STR: arg_error(err, errlen, "argument \"%s\" must be a string", a->name); break; case ARG_INT: arg_error(err, errlen, "argument \"%s\" must be an integer", a->name); break; case ARG_BOOL: arg_error(err, errlen, "argument \"%s\" must be a boolean", a->name); break; case ARG_ENUM: arg_error(err, errlen, "argument \"%s\" must be one of: %s", a->name, a->values ? a->values : ""); break; case ARG_DATE: arg_error(err, errlen, "argument \"%s\" must be YYYY-MM-DD", a->name); break; case ARG_JSON: break; } return -1; } } return 0; } yyjson_mut_val *fail(struct req *r, const char *code, const char *msg) { r->err_code = code; snprintf(r->err_msg, sizeof r->err_msg, "%s", msg); return NULL; } yyjson_mut_val *failf(struct req *r, const char *code, const char *fmt, ...) __attribute__((format(printf, 3, 4))); yyjson_mut_val *failf(struct req *r, const char *code, const char *fmt, ...) { r->err_code = code; va_list ap; va_start(ap, fmt); vsnprintf(r->err_msg, sizeof r->err_msg, fmt, ap); va_end(ap); return NULL; } yyjson_mut_val *db_error(struct req *r) { return fail(r, "INTERNAL", "database error"); } yyjson_mut_val *db_sqlite_error(struct req *r) { return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); } void bind_text_or_null(sqlite3_stmt *st, int idx, const char *s) { if (s) sqlite3_bind_text(st, idx, s, -1, SQLITE_TRANSIENT); else sqlite3_bind_null(st, idx); } int mkdir_p(const char *path, mode_t mode) { char tmp[4096]; if (!path || strlen(path) >= sizeof tmp) return -1; strcpy(tmp, path); for (char *p = tmp + 1; *p; p++) { if (*p == '/') { *p = '\0'; if (mkdir(tmp, mode) != 0 && errno != EEXIST) return -1; *p = '/'; } } if (mkdir(tmp, mode) != 0 && errno != EEXIST) return -1; return 0; } /* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */ /* shared helpers for M2 commands */ /* ------------------------------------------------------------------ */ yyjson_mut_val *json_to_mut(yyjson_mut_doc *doc, const char *json) { if (!json) return NULL; yyjson_doc *d = yyjson_read(json, strlen(json), 0); if (!d) return NULL; yyjson_mut_val *v = yyjson_val_mut_copy(doc, yyjson_doc_get_root(d)); yyjson_doc_free(d); return v; } int is_digits(const char *s) { if (!s || !*s) return 0; for (; *s; s++) if (*s < '0' || *s > '9') return 0; return 1; } double parse_kr_double(const char *s, int *ok) { *ok = 0; if (!s) return 0; char buf[64]; size_t j = 0; for (const char *p = s; *p && j < sizeof buf - 1; p++) { if (*p == ' ' || *p == '\t') continue; buf[j++] = (*p == ',') ? '.' : *p; } buf[j] = '\0'; if (!j || (j == 1 && buf[0] == '-')) return 0; char *end = NULL; double v = strtod(buf, &end); if (!end || *end) return 0; *ok = 1; return v; } char *replace_x(const char *text, double x) { struct buf b; buf_init(&b); for (const char *p = text; *p;) { if (p[0] == '{' && p[1] == 'x' && p[2] == '}') { char tmp[64]; snprintf(tmp, sizeof tmp, "%.2f", x); buf_append(&b, tmp, strlen(tmp)); p += 3; } else { buf_append(&b, p, 1); p++; } } buf_append(&b, "", 1); return (char *)b.p; } int load_template(sqlite3 *db, int64_t org_id, int64_t id, const char *name, struct tpl_head *head, char **err) { sqlite3_stmt *st = NULL; const char *sql = id ? "SELECT id,name,series,description FROM voucher_templates" " WHERE org_id=?1 AND id=?2 AND active=1" : "SELECT id,name,series,description FROM voucher_templates" " WHERE org_id=?1 AND name=?2 AND active=1"; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { if (err) *err = xstrdup("database error"); return -1; } sqlite3_bind_int64(st, 1, org_id); if (id) sqlite3_bind_int64(st, 2, id); else sqlite3_bind_text(st, 2, name ? name : "", -1, SQLITE_TRANSIENT); if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); if (err) *err = xstrdup("template not found"); return -1; } head->id = sqlite3_column_int64(st, 0); snprintf(head->name, sizeof head->name, "%s", sqlite3_column_text(st, 1)); snprintf(head->series, sizeof head->series, "%s", sqlite3_column_text(st, 2)); snprintf(head->description, sizeof head->description, "%s", sqlite3_column_text(st, 3)); sqlite3_finalize(st); return 0; } int load_template_rows(sqlite3 *db, int64_t org_id, int64_t tpl, struct tpl_loaded **out, size_t *out_n, char **err) { sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2( db, "SELECT a.number,tr.formula,COALESCE(tr.description,'')" " FROM voucher_template_rows tr JOIN accounts a" " ON a.org_id=tr.org_id AND a.id=tr.account_id" " WHERE tr.org_id=?1 AND tr.template_id=?2 ORDER BY tr.line_no", -1, &st, NULL) != SQLITE_OK) { if (err) *err = xstrdup("database error"); return -1; } sqlite3_bind_int64(st, 1, org_id); sqlite3_bind_int64(st, 2, tpl); size_t cap = 0, n = 0; struct tpl_loaded *rows = NULL; while (sqlite3_step(st) == SQLITE_ROW) { if (n == cap) { cap = cap ? cap * 2 : 8; rows = xrealloc(rows, cap * sizeof *rows); } memset(&rows[n], 0, sizeof rows[n]); snprintf(rows[n].account, sizeof rows[n].account, "%s", sqlite3_column_text(st, 0)); snprintf(rows[n].formula, sizeof rows[n].formula, "%s", sqlite3_column_text(st, 1)); snprintf(rows[n].desc, sizeof rows[n].desc, "%s", sqlite3_column_text(st, 2)); n++; } sqlite3_finalize(st); *out = rows; *out_n = n; return 0; } int64_t default_fy_id(sqlite3 *db, int64_t org_id) { sqlite3_stmt *st = NULL; int64_t id = 0; if (sqlite3_prepare_v2( db, "SELECT id FROM fiscal_years WHERE org_id=?1" " ORDER BY start_date DESC LIMIT 1", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int64(st, 1, org_id); if (sqlite3_step(st) == SQLITE_ROW) id = sqlite3_column_int64(st, 0); sqlite3_finalize(st); } return id; } int req_fy(struct req *r, int64_t *out) { int64_t fy = 0; arg_int(r->args, "fiscal_year", &fy); if (!fy) fy = default_fy_id(r->db, r->org_id); if (!fy) { fail(r, "NOT_FOUND", "no fiscal year; open one first"); return -1; } *out = fy; return 0; } unsigned char *read_file(const char *path, size_t *out_len) { FILE *f = fopen(path, "rb"); if (!f) return NULL; struct buf b; buf_init(&b); unsigned char chunk[65536]; size_t rn; while ((rn = fread(chunk, 1, sizeof chunk, f)) > 0) buf_append(&b, chunk, rn); int bad = ferror(f); fclose(f); if (bad) { buf_free(&b); return NULL; } *out_len = b.len; return b.p; }