aboutsummaryrefslogtreecommitdiff
path: root/src/commands.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 20:11:19 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 20:11:19 +0200
commit8351fa7752bb8341ef405a37ff7c73142715e981 (patch)
treedde603d313f4f26934f2ce20a22dfd9b69324cf0 /src/commands.c
parentbeafdbe0d64340387d619c38554976410129f05b (diff)
downloadbokf-8351fa7752bb8341ef405a37ff7c73142715e981.tar.gz
bokf-8351fa7752bb8341ef405a37ff7c73142715e981.zip
commands: split the command table by domain
Diffstat (limited to 'src/commands.c')
-rw-r--r--src/commands.c8341
1 files changed, 48 insertions, 8293 deletions
diff --git a/src/commands.c b/src/commands.c
index fe6eeb8..fe97afa 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -1,1522 +1,12 @@
#include "commands.h"
-#include <errno.h>
-#include <stdarg.h>
-#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include <sys/stat.h>
-#include <time.h>
-#include <unistd.h>
-#include "audit.h"
-#include "sha256.h"
-#include "auth.h"
-#include "config.h"
-#include "db.h"
-#include "formula.h"
-#include "invoice.h"
-#include "ledger.h"
-#include "log.h"
-#include "reports.h"
-#include "secret.h"
-#include "seed.h"
-#include "sie.h"
-#include "smtp.h"
-#include "sru.h"
-#include "util.h"
+#include "cmd_util.h"
#include "version.h"
/* ------------------------------------------------------------------ */
-/* small helpers */
-/* ------------------------------------------------------------------ */
-
-static const char *sq(const unsigned char *p)
-{
- return p ? (const char *)p : "";
-}
-
-static 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;
-}
-
-static 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;
-}
-
-static 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;
-}
-
-static 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";
-}
-
-static 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;
-}
-
-static void arg_error(char *err, size_t errlen, const char *fmt, ...)
- __attribute__((format(printf, 3, 4)));
-
-static 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;
-}
-
-static 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;
-}
-
-static yyjson_mut_val *failf(struct req *r, const char *code, const char *fmt, ...)
- __attribute__((format(printf, 3, 4)));
-
-static 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;
-}
-
-static 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;
-}
-
-/* ------------------------------------------------------------------ */
-/* login rate limiting (in-memory, per key) */
-/* ------------------------------------------------------------------ */
-
-#define RL_MAX_KEYS 16
-#define RL_MAX_FAILS 5
-#define RL_WINDOW 900
-
-struct rl_entry {
- char key[64];
- int fails;
- int64_t window_end;
-};
-
-static struct rl_entry g_rl[RL_MAX_KEYS];
-
-static struct rl_entry *rl_get(const char *key, int create)
-{
- struct rl_entry *slot = NULL;
- for (size_t i = 0; i < RL_MAX_KEYS; i++) {
- if (g_rl[i].key[0] && strcmp(g_rl[i].key, key) == 0)
- return &g_rl[i];
- if (create && !g_rl[i].key[0] && !slot)
- slot = &g_rl[i];
- }
- if (create && slot) {
- snprintf(slot->key, sizeof slot->key, "%s", key);
- slot->fails = 0;
- slot->window_end = 0;
- }
- return slot;
-}
-
-static int rl_blocked(const char *key, int64_t *retry_after)
-{
- struct rl_entry *e = rl_get(key, 0);
- if (!e || e->fails < RL_MAX_FAILS)
- return 0;
- int64_t now = util_now();
- if (e->window_end <= now)
- return 0;
- if (retry_after)
- *retry_after = e->window_end - now;
- return 1;
-}
-
-static void rl_fail(const char *key)
-{
- struct rl_entry *e = rl_get(key, 1);
- if (!e)
- return;
- int64_t now = util_now();
- if (e->fails == 0 || e->window_end <= now)
- e->window_end = now + RL_WINDOW;
- e->fails++;
-}
-
-static void rl_ok(const char *key)
-{
- struct rl_entry *e = rl_get(key, 0);
- if (e) {
- e->fails = 0;
- e->window_end = 0;
- }
-}
-
-/* ------------------------------------------------------------------ */
-/* public commands */
-/* ------------------------------------------------------------------ */
-
-static yyjson_mut_val *h_health(struct req *r)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "status", "ok");
- return o;
-}
-
-static yyjson_mut_val *h_meta(struct req *r)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "server", "bokfd");
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "version", BOKF_VERSION);
- yyjson_mut_obj_add_int(r->rdoc, o, "protocol", BOKF_PROTOCOL_VERSION);
- yyjson_mut_val *features = yyjson_mut_arr(r->rdoc);
- yyjson_mut_arr_add_strcpy(r->rdoc, features, "describe");
- yyjson_mut_arr_add_strcpy(r->rdoc, features, "agent.instructions");
- yyjson_mut_arr_add_strcpy(r->rdoc, features, "orgs");
- yyjson_mut_arr_add_strcpy(r->rdoc, features, "tokens");
- yyjson_mut_arr_add_strcpy(r->rdoc, features, "backup");
- yyjson_mut_obj_add_val(r->rdoc, o, "features", features);
- yyjson_mut_val *limits = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, limits, "max_line_bytes",
- g_cfg.max_line_bytes);
- yyjson_mut_obj_add_int(r->rdoc, limits, "max_attachment_bytes",
- g_cfg.max_attachment_bytes);
- yyjson_mut_obj_add_int(r->rdoc, limits, "session_ttl_seconds",
- g_cfg.session_ttl);
- yyjson_mut_obj_add_val(r->rdoc, o, "limits", limits);
- yyjson_mut_obj_add_bool(r->rdoc, o, "tcp_enabled", g_cfg.tcp_enabled != 0);
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "time", ts);
- return o;
-}
-
-static yyjson_mut_val *orgs_for_user(sqlite3 *db, yyjson_mut_doc *doc,
- int64_t user_id, int64_t only_org,
- int64_t *first_org)
-{
- yyjson_mut_val *arr = yyjson_mut_arr(doc);
- const char *sql = only_org
- ? "SELECT o.id,o.name,m.role FROM memberships m"
- " JOIN orgs o ON o.id=m.org_id"
- " WHERE m.user_id=?1 AND o.id=?2 ORDER BY o.id"
- : "SELECT o.id,o.name,m.role FROM memberships m"
- " JOIN orgs o ON o.id=m.org_id"
- " WHERE m.user_id=?1 ORDER BY o.id";
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK)
- return arr;
- sqlite3_bind_int64(st, 1, user_id);
- if (only_org)
- sqlite3_bind_int64(st, 2, only_org);
- while (sqlite3_step(st) == SQLITE_ROW) {
- int64_t id = sqlite3_column_int64(st, 0);
- yyjson_mut_val *o = yyjson_mut_arr_add_obj(doc, arr);
- yyjson_mut_obj_add_int(doc, o, "id", id);
- yyjson_mut_obj_add_strcpy(doc, o, "name", sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(doc, o, "role", sq(sqlite3_column_text(st, 2)));
- if (first_org && *first_org == 0)
- *first_org = id;
- }
- sqlite3_finalize(st);
- return arr;
-}
-
-static yyjson_mut_val *session_payload(struct req *r, struct session *s,
- int64_t user_id, const char *username,
- const char *display,
- yyjson_mut_val *orgs,
- int64_t active_org)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "session", s->id);
- yyjson_mut_val *u = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, u, "id", user_id);
- yyjson_mut_obj_add_strcpy(r->rdoc, u, "username", username ? username : "");
- yyjson_mut_obj_add_strcpy(r->rdoc, u, "display_name",
- display ? display : "");
- yyjson_mut_obj_add_bool(r->rdoc, u, "is_admin", s->is_admin != 0);
- yyjson_mut_obj_add_val(r->rdoc, o, "user", u);
- yyjson_mut_obj_add_val(r->rdoc, o, "orgs", orgs);
- if (active_org)
- yyjson_mut_obj_add_int(r->rdoc, o, "active_org", active_org);
- else
- yyjson_mut_obj_add_null(r->rdoc, o, "active_org");
- return o;
-}
-
-static yyjson_mut_val *h_session_open(struct req *r)
-{
- const char *method = arg_str(r->args, "method");
- if (!method)
- return fail(r, "INVALID_ARGS", "method is required");
- char *reqjson = audit_args_json(r->args);
-
- if (strcmp(method, "password") == 0) {
- const char *username = arg_str(r->args, "username");
- const char *password = arg_str(r->args, "password");
- if (!username || !password) {
- free(reqjson);
- return fail(r, "INVALID_ARGS", "username and password are required");
- }
- int64_t retry = 0;
- if (rl_blocked("local", &retry)) {
- free(reqjson);
- return failf(r, "RATE_LIMITED",
- "too many failed logins, retry in %lld seconds",
- (long long)retry);
- }
- char *pwhash = NULL, *display = NULL;
- int is_admin = 0, disabled = 0;
- int64_t uid = auth_user_lookup(r->db, username, &pwhash, &display,
- &is_admin, &disabled);
- int ok = uid > 0 && !disabled && pwhash &&
- auth_verify_password(pwhash, password) == 0;
- free(pwhash);
- if (!ok) {
- rl_fail("local");
- audit_append(r->db, 0, uid > 0 ? uid : 0, 0, "auth.fail", reqjson,
- "AUTH_FAILED", NULL);
- free(display);
- free(reqjson);
- return fail(r, "AUTH_FAILED", "invalid credentials");
- }
- rl_ok("local");
- int64_t active = 0;
- yyjson_mut_val *orgs =
- orgs_for_user(r->db, r->rdoc, uid, 0, &active);
- struct session *s =
- sessions_create(uid, is_admin, active, 0, "read,write,admin");
- audit_append(r->db, active, uid, 0, "auth.open", reqjson, "OK", NULL);
- yyjson_mut_val *out = session_payload(r, s, uid, username, display,
- orgs, active);
- free(display);
- free(reqjson);
- return out;
- }
-
- if (strcmp(method, "token") == 0) {
- const char *token = arg_str(r->args, "token");
- if (!token) {
- free(reqjson);
- return fail(r, "INVALID_ARGS", "token is required");
- }
- unsigned char th[32];
- auth_hash_token(token, th);
- sqlite3_stmt *st = NULL;
- int rc = sqlite3_prepare_v2(
- r->db,
- "SELECT t.id,t.user_id,t.org_id,t.scopes,t.expires_at,t.revoked_at,"
- " t.label,u.username,u.display_name,u.is_admin,u.disabled_at"
- " FROM api_tokens t JOIN users u ON u.id=t.user_id"
- " WHERE t.token_hash=?1",
- -1, &st, NULL);
- if (rc != SQLITE_OK) {
- free(reqjson);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_blob(st, 1, th, 32, SQLITE_TRANSIENT);
- int found = sqlite3_step(st) == SQLITE_ROW;
- int64_t token_id = 0, uid = 0, org_id = 0;
- char *scopes = NULL, *expires = NULL, *revoked = NULL;
- char *username = NULL, *display = NULL;
- int is_admin = 0, disabled = 0;
- if (found) {
- token_id = sqlite3_column_int64(st, 0);
- uid = sqlite3_column_int64(st, 1);
- org_id = sqlite3_column_int64(st, 2);
- scopes = xstrdup(sq(sqlite3_column_text(st, 3)));
- if (sqlite3_column_type(st, 4) != SQLITE_NULL)
- expires = xstrdup(sq(sqlite3_column_text(st, 4)));
- if (sqlite3_column_type(st, 5) != SQLITE_NULL)
- revoked = xstrdup(sq(sqlite3_column_text(st, 5)));
- username = xstrdup(sq(sqlite3_column_text(st, 7)));
- display = xstrdup(sq(sqlite3_column_text(st, 8)));
- is_admin = sqlite3_column_int(st, 9);
- disabled = sqlite3_column_type(st, 10) != SQLITE_NULL;
- }
- sqlite3_finalize(st);
-
- int expired = 0;
- if (expires) {
- char today[16];
- time_t t = (time_t)util_now();
- struct tm tm;
- gmtime_r(&t, &tm);
- strftime(today, sizeof today, "%Y-%m-%d", &tm);
- expired = strcmp(expires, today) < 0;
- }
- if (!found || revoked || disabled || expired) {
- audit_append(r->db, 0, uid, 0, "auth.fail", reqjson, "AUTH_FAILED",
- NULL);
- free(scopes);
- free(expires);
- free(revoked);
- free(username);
- free(display);
- free(reqjson);
- return fail(r, "AUTH_FAILED", "invalid token");
- }
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *up = NULL;
- if (sqlite3_prepare_v2(
- r->db, "UPDATE api_tokens SET last_used_at=?1 WHERE id=?2", -1,
- &up, NULL) == SQLITE_OK) {
- sqlite3_bind_text(up, 1, ts, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(up, 2, token_id);
- sqlite3_step(up);
- sqlite3_finalize(up);
- }
- int64_t active = org_id;
- yyjson_mut_val *orgs =
- orgs_for_user(r->db, r->rdoc, uid, org_id, &active);
- struct session *s = sessions_create(uid, is_admin, org_id, org_id,
- scopes ? scopes : "read");
- s->active_org = org_id;
- s->token_id = token_id;
- audit_append(r->db, org_id, uid, token_id, "auth.open", reqjson, "OK",
- NULL);
- yyjson_mut_val *out = session_payload(r, s, uid, username, display,
- orgs, org_id);
- free(scopes);
- free(expires);
- free(revoked);
- free(username);
- free(display);
- free(reqjson);
- return out;
- }
-
- free(reqjson);
- return fail(r, "UNSUPPORTED", "unsupported auth method");
-}
-
-static yyjson_mut_val *h_session_close(struct req *r)
-{
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->sess->active_org, r->sess->user_id, 0,
- "session.close", reqjson, "OK", NULL);
- free(reqjson);
- sessions_destroy(r->sess->id);
- return yyjson_mut_obj(r->rdoc);
-}
-
-static yyjson_mut_val *h_session_whoami(struct req *r)
-{
- char *display = NULL, *username = NULL;
- int is_admin = 0;
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db,
- "SELECT username,display_name,is_admin FROM users"
- " WHERE id=?1",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->sess->user_id);
- if (sqlite3_step(st) == SQLITE_ROW) {
- username = xstrdup(sq(sqlite3_column_text(st, 0)));
- display = xstrdup(sq(sqlite3_column_text(st, 1)));
- is_admin = sqlite3_column_int(st, 2);
- }
- sqlite3_finalize(st);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_val *u = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, u, "id", r->sess->user_id);
- yyjson_mut_obj_add_strcpy(r->rdoc, u, "username", username ? username : "");
- yyjson_mut_obj_add_strcpy(r->rdoc, u, "display_name", display ? display : "");
- yyjson_mut_obj_add_bool(r->rdoc, u, "is_admin", is_admin != 0);
- yyjson_mut_obj_add_val(r->rdoc, o, "user", u);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "scopes", r->sess->scopes);
- if (r->sess->active_org) {
- yyjson_mut_obj_add_int(r->rdoc, o, "active_org", r->sess->active_org);
- char *role = db_membership_role(r->db, r->sess->active_org,
- r->sess->user_id);
- if (role)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "role", role);
- else
- yyjson_mut_obj_add_null(r->rdoc, o, "role");
- free(role);
- } else {
- yyjson_mut_obj_add_null(r->rdoc, o, "active_org");
- yyjson_mut_obj_add_null(r->rdoc, o, "role");
- }
- free(username);
- free(display);
- return o;
-}
-
-static yyjson_mut_val *h_session_list_orgs(struct req *r)
-{
- int64_t first = 0;
- yyjson_mut_val *orgs = orgs_for_user(r->db, r->rdoc, r->sess->user_id, 0,
- &first);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, o, "items", orgs);
- return o;
-}
-
-static yyjson_mut_val *h_session_use_org(struct req *r)
-{
- int64_t org = 0;
- if (!arg_int(r->args, "org", &org) || org <= 0)
- return fail(r, "INVALID_ARGS", "org is required");
- if (r->sess->bound_org && org != r->sess->bound_org)
- return fail(r, "ORG_FORBIDDEN", "token is bound to another org");
- char *role = db_membership_role(r->db, org, r->sess->user_id);
- if (!role)
- return fail(r, "ORG_FORBIDDEN", "not a member of this org");
- r->sess->active_org = org;
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "active_org", org);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "role", role);
- free(role);
- return o;
-}
-
-/* ------------------------------------------------------------------ */
-/* orgs */
-/* ------------------------------------------------------------------ */
-
-static yyjson_mut_val *org_json(struct req *r, sqlite3_stmt *st)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "org_nr", sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "vat_nr", sq(sqlite3_column_text(st, 3)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "address",
- sq(sqlite3_column_text(st, 4)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "postal_code",
- sq(sqlite3_column_text(st, 5)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "city", sq(sqlite3_column_text(st, 6)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "country",
- sq(sqlite3_column_text(st, 7)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "email", sq(sqlite3_column_text(st, 8)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "phone", sq(sqlite3_column_text(st, 9)));
- yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_start_month",
- sqlite3_column_int(st, 10));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "moms_period",
- sq(sqlite3_column_text(st, 11)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "framework",
- sq(sqlite3_column_text(st, 12)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at",
- sq(sqlite3_column_text(st, 13)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
- sq(sqlite3_column_text(st, 14)));
- yyjson_mut_obj_add_int(r->rdoc, o, "shares",
- sqlite3_column_int64(st, 15));
- return o;
-}
-
-#define ORG_COLUMNS \
- "id,name,org_nr,vat_nr,address,postal_code,city,country,email," \
- "phone,fiscal_year_start_month,moms_period,framework,created_at," \
- "description,shares"
-
-static yyjson_mut_val *h_org_create(struct req *r)
-{
- if (!g_cfg.allow_org_create && !r->is_admin)
- return fail(r, "FORBIDDEN", "org creation is disabled");
- const char *name = arg_str(r->args, "name");
- if (!name || !*name)
- return fail(r, "INVALID_ARGS", "name is required");
- const char *org_nr = arg_str(r->args, "org_nr");
- int64_t fy_month = 1;
- arg_int(r->args, "fiscal_year_start_month", &fy_month);
- if (fy_month < 1 || fy_month > 12)
- return fail(r, "INVALID_ARGS", "fiscal_year_start_month must be 1-12");
- const char *moms = arg_str(r->args, "moms_period");
- if (!moms)
- moms = "month";
- if (strcmp(moms, "month") != 0 && strcmp(moms, "quarter") != 0 &&
- strcmp(moms, "year") != 0)
- return fail(r, "INVALID_ARGS", "moms_period must be month, quarter or year");
- const char *framework = arg_str(r->args, "framework");
- if (!framework)
- framework = "K2";
- if (strcmp(framework, "K2") != 0 && strcmp(framework, "K3") != 0)
- return fail(r, "INVALID_ARGS", "framework must be K2 or K3");
-
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- if (db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0)
- return fail(r, "DB_BUSY", "could not start transaction");
-
- sqlite3_stmt *st = NULL;
- int rc = sqlite3_prepare_v2(
- r->db,
- "INSERT INTO orgs(name,org_nr,fiscal_year_start_month,moms_period,"
- "framework,created_at,created_by) VALUES(?1,?2,?3,?4,?5,?6,?7)",
- -1, &st, NULL);
- if (rc != SQLITE_OK) {
- db_exec(r->db, "ROLLBACK", NULL);
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT);
- if (org_nr)
- sqlite3_bind_text(st, 2, org_nr, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 2);
- sqlite3_bind_int64(st, 3, fy_month);
- sqlite3_bind_text(st, 4, moms, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 5, framework, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 6, ts, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 7, r->sess->user_id);
- rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- db_exec(r->db, "ROLLBACK", NULL);
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- int64_t org_id = db_last_id(r->db);
-
- rc = sqlite3_prepare_v2(
- r->db,
- "INSERT INTO memberships(org_id,user_id,role,created_at)"
- " VALUES(?1,?2,'owner',?3)",
- -1, &st, NULL);
- if (rc != SQLITE_OK) {
- db_exec(r->db, "ROLLBACK", NULL);
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- sqlite3_bind_int64(st, 1, org_id);
- sqlite3_bind_int64(st, 2, r->sess->user_id);
- sqlite3_bind_text(st, 3, ts, -1, SQLITE_TRANSIENT);
- rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- db_exec(r->db, "ROLLBACK", NULL);
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- int64_t fy_id = 0;
- char *seed_err = NULL;
- if (seed_org(r->db, org_id, framework, (int)fy_month, &fy_id,
- &seed_err) != 0) {
- db_exec(r->db, "ROLLBACK", NULL);
- yyjson_mut_val *res =
- fail(r, "INTERNAL", seed_err ? seed_err : "seeding failed");
- free(seed_err);
- return res;
- }
- if (db_exec(r->db, "COMMIT", NULL) != 0)
- return fail(r, "INTERNAL", "commit failed");
-
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, org_id, r->sess->user_id, r->sess->token_id,
- "org.create", reqjson, "OK", NULL);
- free(reqjson);
- r->sess->active_org = org_id;
-
- st = NULL;
- if (sqlite3_prepare_v2(r->db, "SELECT " ORG_COLUMNS " FROM orgs WHERE id=?1",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, org_id);
- yyjson_mut_val *o = NULL;
- if (sqlite3_step(st) == SQLITE_ROW)
- o = org_json(r, st);
- sqlite3_finalize(st);
- if (!o)
- return fail(r, "INTERNAL", "created org not found");
- yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_id", fy_id);
- return o;
-}
-
-static yyjson_mut_val *h_org_list(struct req *r)
-{
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT o.id,o.name,o.org_nr,m.role FROM memberships m"
- " JOIN orgs o ON o.id=m.org_id WHERE m.user_id=?1 ORDER BY o.id",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->sess->user_id);
- while (sqlite3_step(st) == SQLITE_ROW) {
- yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "org_nr",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "role",
- sq(sqlite3_column_text(st, 3)));
- }
- sqlite3_finalize(st);
- yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
- return out;
-}
-
-static yyjson_mut_val *h_org_get(struct req *r)
-{
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db, "SELECT " ORG_COLUMNS " FROM orgs WHERE id=?1",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- yyjson_mut_val *o = NULL;
- if (sqlite3_step(st) == SQLITE_ROW)
- o = org_json(r, st);
- sqlite3_finalize(st);
- if (!o)
- return fail(r, "NOT_FOUND", "org not found");
- return o;
-}
-
-static yyjson_mut_val *h_org_update(struct req *r)
-{
- const char *name = arg_str(r->args, "name");
- const char *org_nr = arg_str(r->args, "org_nr");
- const char *vat_nr = arg_str(r->args, "vat_nr");
- const char *address = arg_str(r->args, "address");
- const char *postal = arg_str(r->args, "postal_code");
- const char *city = arg_str(r->args, "city");
- const char *country = arg_str(r->args, "country");
- const char *email = arg_str(r->args, "email");
- const char *phone = arg_str(r->args, "phone");
- const char *moms = arg_str(r->args, "moms_period");
- const char *framework = arg_str(r->args, "framework");
- const char *description = arg_str(r->args, "description");
- int64_t fy_month = 0;
- arg_int(r->args, "fiscal_year_start_month", &fy_month);
- int64_t shares = -1;
- arg_int(r->args, "shares", &shares);
- if (!name && !org_nr && !vat_nr && !address && !postal && !city &&
- !country && !email && !phone && !moms && !framework && fy_month == 0 &&
- !description && shares < 0)
- return fail(r, "INVALID_ARGS", "nothing to update");
- if (name && !*name)
- return fail(r, "INVALID_ARGS", "name cannot be empty");
- if (fy_month && (fy_month < 1 || fy_month > 12))
- return fail(r, "INVALID_ARGS", "fiscal_year_start_month must be 1-12");
- if (shares < -1)
- return fail(r, "INVALID_ARGS", "shares must be >= 0");
- if (moms && strcmp(moms, "month") != 0 && strcmp(moms, "quarter") != 0 &&
- strcmp(moms, "year") != 0)
- return fail(r, "INVALID_ARGS", "moms_period must be month, quarter or year");
- if (framework && strcmp(framework, "K2") != 0 &&
- strcmp(framework, "K3") != 0)
- return fail(r, "INVALID_ARGS", "framework must be K2 or K3");
-
- static const char *const keys[10] = { "name", "org_nr", "vat_nr",
- "address", "postal_code", "city",
- "country", "email", "phone",
- "description" };
- const char *texts[10] = { name, org_nr, vat_nr, address, postal,
- city, country, email, phone, description };
-
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- for (int i = 0; i < 10; i++)
- if (texts[i])
- yyjson_mut_obj_add_strcpy(r->rdoc, o, keys[i], texts[i]);
- if (fy_month)
- yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_start_month",
- fy_month);
- if (moms)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "moms_period", moms);
- if (framework)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "framework", framework);
- if (shares >= 0)
- yyjson_mut_obj_add_int(r->rdoc, o, "shares", shares);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
- }
-
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE orgs SET"
- " name=COALESCE(?2,name), org_nr=COALESCE(?3,org_nr),"
- " vat_nr=COALESCE(?4,vat_nr), address=COALESCE(?5,address),"
- " postal_code=COALESCE(?6,postal_code), city=COALESCE(?7,city),"
- " country=COALESCE(?8,country), email=COALESCE(?9,email),"
- " phone=COALESCE(?10,phone), description=COALESCE(?11,"
- " description),"
- " fiscal_year_start_month=CASE WHEN ?12=0"
- " THEN fiscal_year_start_month ELSE ?12 END,"
- " moms_period=COALESCE(?13,moms_period),"
- " framework=COALESCE(?14,framework),"
- " shares=CASE WHEN ?15<0 THEN shares ELSE ?15 END"
- " WHERE id=?1",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- for (int i = 0; i < 10; i++) {
- if (texts[i])
- sqlite3_bind_text(st, i + 2, texts[i], -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, i + 2);
- }
- sqlite3_bind_int64(st, 12, fy_month);
- if (moms)
- sqlite3_bind_text(st, 13, moms, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 13);
- if (framework)
- sqlite3_bind_text(st, 14, framework, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 14);
- sqlite3_bind_int64(st, 15, shares);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE)
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
-
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "org.update", reqjson, "OK", NULL);
- free(reqjson);
-
- st = NULL;
- if (sqlite3_prepare_v2(r->db, "SELECT " ORG_COLUMNS
- " FROM orgs WHERE id=?1",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- yyjson_mut_val *o = NULL;
- if (sqlite3_step(st) == SQLITE_ROW)
- o = org_json(r, st);
- sqlite3_finalize(st);
- if (!o)
- return fail(r, "NOT_FOUND", "org not found");
- return o;
-}
-
-static yyjson_mut_val *h_org_member_list(struct req *r)
-{
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT u.id,u.username,u.display_name,m.role FROM memberships m"
- " JOIN users u ON u.id=m.user_id WHERE m.org_id=?1 ORDER BY u.id",
- -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_val *o = yyjson_mut_arr_add_obj(r->rdoc, items);
- yyjson_mut_obj_add_int(r->rdoc, o, "user_id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "username",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "display_name",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "role",
- sq(sqlite3_column_text(st, 3)));
- }
- sqlite3_finalize(st);
- yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
- return out;
-}
-
-static int64_t username_to_id(sqlite3 *db, const char *username)
-{
- sqlite3_stmt *st = NULL;
- int64_t id = -1;
- if (sqlite3_prepare_v2(db, "SELECT id FROM users WHERE username=?1", -1,
- &st, NULL) != SQLITE_OK)
- return -1;
- sqlite3_bind_text(st, 1, username, -1, SQLITE_TRANSIENT);
- if (sqlite3_step(st) == SQLITE_ROW)
- id = sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
- return id;
-}
-
-static int64_t owner_count(sqlite3 *db, int64_t org_id)
-{
- sqlite3_stmt *st = NULL;
- int64_t n = -1;
- if (sqlite3_prepare_v2(
- db, "SELECT count(*) FROM memberships WHERE org_id=?1 AND role='owner'",
- -1, &st, NULL) != SQLITE_OK)
- return -1;
- sqlite3_bind_int64(st, 1, org_id);
- if (sqlite3_step(st) == SQLITE_ROW)
- n = sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
- return n;
-}
-
-static int valid_role(const char *role)
-{
- return role && (strcmp(role, "owner") == 0 ||
- strcmp(role, "bookkeeper") == 0 ||
- strcmp(role, "viewer") == 0);
-}
-
-static yyjson_mut_val *h_org_member_add(struct req *r)
-{
- const char *username = arg_str(r->args, "username");
- const char *role = arg_str(r->args, "role");
- if (!username || !valid_role(role))
- return fail(r, "INVALID_ARGS", "username and a valid role are required");
- int64_t uid = username_to_id(r->db, username);
- if (uid < 0)
- return fail(r, "NOT_FOUND", "user not found");
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO memberships(org_id,user_id,role,created_at)"
- " VALUES(?1,?2,?3,?4)",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, uid);
- sqlite3_bind_text(st, 3, role, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 4, ts, -1, SQLITE_TRANSIENT);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE)
- return fail(r, "CONFLICT", "user is already a member");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, 0, "member.add", reqjson,
- "OK", NULL);
- free(reqjson);
- return yyjson_mut_obj(r->rdoc);
-}
-
-static yyjson_mut_val *h_org_member_set_role(struct req *r)
-{
- const char *username = arg_str(r->args, "username");
- const char *role = arg_str(r->args, "role");
- if (!username || !valid_role(role))
- return fail(r, "INVALID_ARGS", "username and a valid role are required");
- int64_t uid = username_to_id(r->db, username);
- if (uid < 0)
- return fail(r, "NOT_FOUND", "user not found");
- char *current = db_membership_role(r->db, r->org_id, uid);
- if (!current)
- return fail(r, "NOT_FOUND", "user is not a member");
- if (strcmp(current, "owner") == 0 && strcmp(role, "owner") != 0 &&
- owner_count(r->db, r->org_id) <= 1) {
- free(current);
- return fail(r, "CONFLICT", "cannot demote the last owner");
- }
- free(current);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE memberships SET role=?1 WHERE org_id=?2 AND user_id=?3",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_text(st, 1, role, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 2, r->org_id);
- sqlite3_bind_int64(st, 3, uid);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE)
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, 0, "member.set_role",
- reqjson, "OK", NULL);
- free(reqjson);
- return yyjson_mut_obj(r->rdoc);
-}
-
-static yyjson_mut_val *h_org_member_remove(struct req *r)
-{
- const char *username = arg_str(r->args, "username");
- if (!username)
- return fail(r, "INVALID_ARGS", "username is required");
- int64_t uid = username_to_id(r->db, username);
- if (uid < 0)
- return fail(r, "NOT_FOUND", "user not found");
- char *current = db_membership_role(r->db, r->org_id, uid);
- if (!current)
- return fail(r, "NOT_FOUND", "user is not a member");
- if (strcmp(current, "owner") == 0 && owner_count(r->db, r->org_id) <= 1) {
- free(current);
- return fail(r, "CONFLICT", "cannot remove the last owner");
- }
- free(current);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db, "DELETE FROM memberships WHERE org_id=?1 AND user_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, uid);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE)
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, 0, "member.remove",
- reqjson, "OK", NULL);
- free(reqjson);
- return yyjson_mut_obj(r->rdoc);
-}
-
-/* ------------------------------------------------------------------ */
-/* board members (årsredovisning signatures) */
-/* ------------------------------------------------------------------ */
-
-static yyjson_mut_val *board_json(struct req *r, sqlite3_stmt *st)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "title",
- sq(sqlite3_column_text(st, 2)));
- return o;
-}
-
-static yyjson_mut_val *h_board_list(struct req *r)
-{
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db,
- "SELECT id,name,title FROM board_members"
- " WHERE org_id=?1 ORDER BY id",
- -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, board_json(r, 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 *board_get(struct req *r, int64_t id)
-{
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db,
- "SELECT id,name,title FROM board_members"
- " 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);
- yyjson_mut_val *o = NULL;
- if (sqlite3_step(st) == SQLITE_ROW)
- o = board_json(r, st);
- sqlite3_finalize(st);
- if (!o)
- return fail(r, "NOT_FOUND", "board member not found");
- return o;
-}
-
-static yyjson_mut_val *h_board_add(struct req *r)
-{
- const char *name = arg_str(r->args, "name");
- const char *title = arg_str(r->args, "title");
- if (!name || !*name)
- return fail(r, "INVALID_ARGS", "name is required");
- if (!title || !*title)
- title = "Styrelseledamot";
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "title", title);
- return o;
- }
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db,
- "INSERT INTO board_members(org_id,name,title,"
- "created_at) VALUES(?1,?2,?3,?4)",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 3, title, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 4, ts, -1, SQLITE_TRANSIENT);
- 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,
- "board.add", reqjson, "OK", NULL);
- free(reqjson);
- return board_get(r, id);
-}
-
-static yyjson_mut_val *h_board_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 *title = arg_str(r->args, "title");
- if (!name && !title)
- return fail(r, "INVALID_ARGS", "nothing to update");
- if (name && !*name)
- return fail(r, "INVALID_ARGS", "name cannot be empty");
- if (title && !*title)
- return fail(r, "INVALID_ARGS", "title cannot be empty");
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
- if (name)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name);
- if (title)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "title", title);
- return o;
- }
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db,
- "UPDATE board_members SET name=COALESCE(?3,name),"
- " title=COALESCE(?4,title)"
- " 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);
- if (name)
- sqlite3_bind_text(st, 3, name, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 3);
- if (title)
- sqlite3_bind_text(st, 4, title, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 4);
- 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", "board member not found");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "board.update", reqjson, "OK", NULL);
- free(reqjson);
- return board_get(r, id);
-}
-
-static yyjson_mut_val *h_board_remove(struct req *r)
-{
- int64_t id = 0;
- if (!arg_int(r->args, "id", &id) || id <= 0)
- return fail(r, "INVALID_ARGS", "id is required");
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
- return o;
- }
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db,
- "DELETE FROM board_members"
- " 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", "board member not found");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "board.remove", reqjson, "OK", NULL);
- free(reqjson);
- return yyjson_mut_obj(r->rdoc);
-}
-
-/* ------------------------------------------------------------------ */
-/* users and tokens */
-/* ------------------------------------------------------------------ */
-
-static yyjson_mut_val *h_user_create(struct req *r)
-{
- const char *username = arg_str(r->args, "username");
- const char *password = arg_str(r->args, "password");
- const char *display = arg_str(r->args, "display_name");
- if (!username || !password)
- return fail(r, "INVALID_ARGS", "username and password are required");
- int is_admin = 0;
- arg_bool(r->args, "is_admin", &is_admin);
- char *err = NULL;
- int64_t uid = 0;
- if (db_create_user(r->db, username, display, password, is_admin, &uid,
- &err) != 0) {
- const char *code = err && strstr(err, "already exists") ? "CONFLICT"
- : "INTERNAL";
- yyjson_mut_val *res = fail(r, code, err ? err : "could not create user");
- free(err);
- return res;
- }
- free(err);
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, 0, r->sess->user_id, 0, "user.create", reqjson, "OK",
- NULL);
- free(reqjson);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", uid);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "username", username);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "display_name",
- display ? display : username);
- yyjson_mut_obj_add_bool(r->rdoc, o, "is_admin", is_admin != 0);
- return o;
-}
-
-static yyjson_mut_val *h_user_list(struct req *r)
-{
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT id,username,display_name,is_admin,created_at,disabled_at"
- " FROM users ORDER BY id",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- while (sqlite3_step(st) == SQLITE_ROW) {
- yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "username",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "display_name",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_bool(r->rdoc, o, "is_admin",
- sqlite3_column_int(st, 3) != 0);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at",
- sq(sqlite3_column_text(st, 4)));
- if (sqlite3_column_type(st, 5) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "disabled_at");
- else
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "disabled_at",
- sq(sqlite3_column_text(st, 5)));
- }
- sqlite3_finalize(st);
- yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
- return out;
-}
-
-static int scope_allowed(const char *role, const char *scope)
-{
- int is_owner = strcmp(role, "owner") == 0;
- int is_bookkeeper = is_owner || strcmp(role, "bookkeeper") == 0;
- if (strcmp(scope, "read") == 0)
- return 1;
- if (strcmp(scope, "write") == 0)
- return is_bookkeeper;
- if (strcmp(scope, "admin") == 0)
- return is_owner;
- return 0;
-}
-
-static yyjson_mut_val *h_token_create(struct req *r)
-{
- const char *label = arg_str(r->args, "label");
- if (!label || !*label)
- return fail(r, "INVALID_ARGS", "label is required");
- char scopes[64] = "";
- yyjson_val *sv = r->args && yyjson_is_obj(r->args)
- ? yyjson_obj_get(r->args, "scopes")
- : NULL;
- if (sv) {
- if (!yyjson_is_arr(sv))
- return fail(r, "INVALID_ARGS", "scopes must be an array");
- yyjson_val *item;
- yyjson_arr_iter it = yyjson_arr_iter_with(sv);
- while ((item = yyjson_arr_iter_next(&it))) {
- const char *s = yyjson_get_str(item);
- if (!s || !scope_allowed(r->role, s))
- return fail(r, "FORBIDDEN", "scope not allowed for your role");
- if (scopes[0])
- strncat(scopes, ",", sizeof scopes - strlen(scopes) - 1);
- strncat(scopes, s, sizeof scopes - strlen(scopes) - 1);
- }
- }
- if (!scopes[0])
- snprintf(scopes, sizeof scopes, "%s",
- strcmp(r->role, "owner") == 0
- ? "read,write,admin"
- : (strcmp(r->role, "bookkeeper") == 0 ? "read,write"
- : "read"));
- const char *expires_at = arg_str(r->args, "expires_at");
- if (expires_at && !util_parse_iso_date(expires_at))
- return fail(r, "INVALID_ARGS", "expires_at must be YYYY-MM-DD");
- char *token = auth_generate_token();
- unsigned char th[32];
- auth_hash_token(token, th);
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO api_tokens(org_id,user_id,label,token_hash,scopes,"
- "created_at,expires_at) VALUES(?1,?2,?3,?4,?5,?6,?7)",
- -1, &st, NULL) != SQLITE_OK) {
- free(token);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, r->sess->user_id);
- sqlite3_bind_text(st, 3, label, -1, SQLITE_TRANSIENT);
- sqlite3_bind_blob(st, 4, th, 32, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 5, scopes, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 6, ts, -1, SQLITE_TRANSIENT);
- if (expires_at)
- sqlite3_bind_text(st, 7, expires_at, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 7);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- free(token);
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- int64_t token_id = db_last_id(r->db);
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, token_id, "token.create",
- reqjson, "OK", NULL);
- free(reqjson);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", token_id);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "label", label);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "scopes", scopes);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "token", token);
- if (expires_at)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "expires_at", expires_at);
- else
- yyjson_mut_obj_add_null(r->rdoc, o, "expires_at");
- free(token);
- return o;
-}
-
-static yyjson_mut_val *h_token_list(struct req *r)
-{
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT id,label,scopes,created_at,expires_at,last_used_at,"
- "revoked_at FROM api_tokens WHERE org_id=?1 AND user_id=?2"
- " ORDER BY id",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, r->sess->user_id);
- while (sqlite3_step(st) == SQLITE_ROW) {
- yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "label",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "scopes",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at",
- sq(sqlite3_column_text(st, 3)));
- if (sqlite3_column_type(st, 4) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "expires_at");
- else
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "expires_at",
- sq(sqlite3_column_text(st, 4)));
- if (sqlite3_column_type(st, 5) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "last_used_at");
- else
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "last_used_at",
- sq(sqlite3_column_text(st, 5)));
- if (sqlite3_column_type(st, 6) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "revoked_at");
- else
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "revoked_at",
- sq(sqlite3_column_text(st, 6)));
- }
- sqlite3_finalize(st);
- yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
- return out;
-}
-
-static yyjson_mut_val *h_token_revoke(struct req *r)
-{
- int64_t id = 0;
- if (!arg_int(r->args, "id", &id) || id <= 0)
- return fail(r, "INVALID_ARGS", "id is required");
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- int is_owner = strcmp(r->role, "owner") == 0;
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- is_owner
- ? "UPDATE api_tokens SET revoked_at=?1 WHERE id=?2 AND org_id=?3"
- : "UPDATE api_tokens SET revoked_at=?1 WHERE id=?2 AND org_id=?3"
- " AND user_id=?4",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_text(st, 1, ts, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 2, id);
- sqlite3_bind_int64(st, 3, r->org_id);
- if (!is_owner)
- sqlite3_bind_int64(st, 4, r->sess->user_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", "token not found");
- audit_append(r->db, r->org_id, r->sess->user_id, id, "token.revoke", "{}",
- "OK", NULL);
- return yyjson_mut_obj(r->rdoc);
-}
-
-/* ------------------------------------------------------------------ */
/* discovery */
/* ------------------------------------------------------------------ */
@@ -1580,8 +70,11 @@ static yyjson_mut_val *h_describe(struct req *r)
return o;
}
yyjson_mut_val *arr = yyjson_mut_arr(r->rdoc);
- for (size_t i = 0; i < g_commands_count; i++)
- yyjson_mut_arr_add_val(arr, command_json(r, &g_commands[i]));
+ for (size_t t = 0; t < g_command_tables_count; t++) {
+ const struct cmd_table *tab = g_command_tables[t];
+ for (size_t i = 0; i < tab->n; i++)
+ yyjson_mut_arr_add_val(arr, command_json(r, &tab->cmds[i]));
+ }
yyjson_mut_obj_add_val(r->rdoc, o, "commands", arr);
return o;
}
@@ -1637,6802 +130,32 @@ static yyjson_mut_val *h_agent_instructions(struct req *r)
return o;
}
-/* ------------------------------------------------------------------ */
-/* audit and backup */
-/* ------------------------------------------------------------------ */
-
-static yyjson_mut_val *h_audit_list(struct req *r)
-{
- int64_t cursor = 0, limit = 100;
- arg_int(r->args, "cursor", &cursor);
- arg_int(r->args, "limit", &limit);
- if (limit < 1)
- limit = 100;
- if (limit > 1000)
- limit = 1000;
- const char *action = arg_str(r->args, "action");
-
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- sqlite3_stmt *st = NULL;
- const char *sql = action
- ? "SELECT seq,at,actor_user_id,actor_token_id,action,"
- "request_json,result_code FROM audit_log"
- " WHERE org_id=?1 AND seq>?2 AND action=?3"
- " ORDER BY seq LIMIT ?4"
- : "SELECT seq,at,actor_user_id,actor_token_id,action,"
- "request_json,result_code FROM audit_log"
- " WHERE org_id=?1 AND seq>?2 ORDER BY seq LIMIT ?4";
- 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);
- sqlite3_bind_int64(st, 2, cursor);
- if (action)
- sqlite3_bind_text(st, 3, action, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 4, limit);
- int64_t last_seq = cursor;
- int64_t n = 0;
- while (sqlite3_step(st) == SQLITE_ROW) {
- n++;
- last_seq = sqlite3_column_int64(st, 0);
- yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items);
- yyjson_mut_obj_add_int(r->rdoc, o, "seq", last_seq);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "at",
- sq(sqlite3_column_text(st, 1)));
- if (sqlite3_column_type(st, 2) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "actor_user_id");
- else
- yyjson_mut_obj_add_int(r->rdoc, o, "actor_user_id",
- sqlite3_column_int64(st, 2));
- if (sqlite3_column_type(st, 3) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "actor_token_id");
- else
- yyjson_mut_obj_add_int(r->rdoc, o, "actor_token_id",
- sqlite3_column_int64(st, 3));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "action",
- sq(sqlite3_column_text(st, 4)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "result_code",
- sq(sqlite3_column_text(st, 6)));
- const char *req = (const char *)sqlite3_column_text(st, 5);
- yyjson_doc *parsed = req ? yyjson_read(req, strlen(req), 0) : NULL;
- if (parsed && yyjson_is_obj(yyjson_doc_get_root(parsed)))
- yyjson_mut_obj_add_val(r->rdoc, o, "request",
- yyjson_val_mut_copy(r->rdoc,
- yyjson_doc_get_root(parsed)));
- if (parsed)
- yyjson_doc_free(parsed);
- }
- sqlite3_finalize(st);
- yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
- if (n == limit)
- yyjson_mut_obj_add_int(r->rdoc, out, "next_cursor", last_seq);
- else
- yyjson_mut_obj_add_null(r->rdoc, out, "next_cursor");
- return out;
-}
-
-static int verify_attachments(sqlite3 *db, int64_t *checked, int64_t *bad_id,
- char **err)
-{
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(db,
- "SELECT id,sha256,size_bytes,content FROM attachments"
- " ORDER BY id",
- -1, &st, NULL) != SQLITE_OK) {
- if (err && !*err)
- *err = xstrdup(sqlite3_errmsg(db));
- return -1;
- }
- int ret = 0;
- for (;;) {
- int step = sqlite3_step(st);
- if (step == SQLITE_DONE)
- break;
- if (step != SQLITE_ROW) {
- if (err && !*err)
- *err = xstrdup(sqlite3_errmsg(db));
- ret = -1;
- break;
- }
- int64_t id = sqlite3_column_int64(st, 0);
- const void *sha = sqlite3_column_blob(st, 1);
- int sha_n = sqlite3_column_bytes(st, 1);
- int64_t size = sqlite3_column_int64(st, 2);
- const void *content = sqlite3_column_blob(st, 3);
- int content_n = sqlite3_column_bytes(st, 3);
- unsigned char digest[32];
- util_sha256(content, (size_t)content_n, digest);
- (*checked)++;
- if (sha_n != 32 || memcmp(sha, digest, 32) != 0 ||
- size != content_n) {
- if (!*bad_id)
- *bad_id = id;
- ret = 1;
- }
- }
- sqlite3_finalize(st);
- return ret;
-}
-
-static yyjson_mut_val *h_audit_verify(struct req *r)
-{
- int full = 0;
- arg_bool(r->args, "full", &full);
-
- int64_t audit_checked = 0, audit_bad = 0;
- char *err = NULL;
- int audit_rc = audit_verify(r->db, &audit_checked, &audit_bad, &err);
- if (audit_rc < 0) {
- yyjson_mut_val *res = fail(r, "INTERNAL", err ? err : "verify failed");
- free(err);
- return res;
- }
- free(err);
- err = NULL;
-
- struct ledger_verify_result lv;
- int ledger_rc = ledger_verify(r->db, &lv, &err);
- if (ledger_rc < 0) {
- yyjson_mut_val *res = fail(r, "INTERNAL", err ? err : "verify failed");
- free(err);
- return res;
- }
- free(err);
- err = NULL;
-
- int64_t att_checked = 0, att_bad = 0;
- int att_rc = 0;
- if (full) {
- att_rc = verify_attachments(r->db, &att_checked, &att_bad, &err);
- if (att_rc < 0) {
- yyjson_mut_val *res =
- fail(r, "INTERNAL", err ? err : "verify failed");
- free(err);
- return res;
- }
- free(err);
- }
-
- int ok = audit_rc == 0 && ledger_rc == 0 && att_rc == 0;
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_bool(r->rdoc, o, "ok", ok);
- yyjson_mut_obj_add_int(r->rdoc, o, "checked", audit_checked);
- yyjson_mut_obj_add_int(r->rdoc, o, "vouchers_checked",
- lv.vouchers_checked);
- yyjson_mut_obj_add_int(r->rdoc, o, "unbalanced_vouchers",
- lv.unbalanced_vouchers);
- if (audit_bad)
- yyjson_mut_obj_add_int(r->rdoc, o, "first_bad_seq", audit_bad);
- else
- yyjson_mut_obj_add_null(r->rdoc, o, "first_bad_seq");
- if (lv.first_bad_voucher_id)
- yyjson_mut_obj_add_int(r->rdoc, o, "first_bad_voucher_id",
- lv.first_bad_voucher_id);
- else
- yyjson_mut_obj_add_null(r->rdoc, o, "first_bad_voucher_id");
- if (lv.first_unbalanced_voucher_id)
- yyjson_mut_obj_add_int(r->rdoc, o, "first_unbalanced_voucher_id",
- lv.first_unbalanced_voucher_id);
- else
- yyjson_mut_obj_add_null(r->rdoc, o, "first_unbalanced_voucher_id");
- if (full) {
- yyjson_mut_obj_add_int(r->rdoc, o, "attachments_checked",
- att_checked);
- if (att_bad)
- yyjson_mut_obj_add_int(r->rdoc, o, "first_bad_attachment_id",
- att_bad);
- else
- yyjson_mut_obj_add_null(r->rdoc, o, "first_bad_attachment_id");
- }
- return o;
-}
-
-static yyjson_mut_val *h_backup_snapshot(struct req *r)
-{
- const char *dest = arg_str(r->args, "dest");
- if (mkdir_p(g_cfg.backup_dir, 0700) != 0)
- return fail(r, "INTERNAL", "cannot create backup directory");
- char path[4096];
- if (dest && *dest) {
- snprintf(path, sizeof path, "%s", dest);
- } else {
- char stamp[32];
- time_t t = (time_t)util_now();
- struct tm tm;
- gmtime_r(&t, &tm);
- strftime(stamp, sizeof stamp, "%Y%m%dT%H%M%SZ", &tm);
- snprintf(path, sizeof path, "%s/bokfd-%s.db", g_cfg.backup_dir, stamp);
- }
- if (access(path, F_OK) == 0)
- return fail(r, "CONFLICT", "destination already exists");
- char *sql = sqlite3_mprintf("VACUUM INTO %Q", path);
- int rc = db_exec(r->db, sql, NULL);
- sqlite3_free(sql);
- if (rc != 0)
- return fail(r, "INTERNAL", "VACUUM INTO failed");
-
- struct stat sb;
- if (stat(path, &sb) != 0)
- return fail(r, "INTERNAL", "backup file not found after snapshot");
- FILE *f = fopen(path, "rb");
- if (!f)
- return fail(r, "INTERNAL", "cannot read backup file");
- SHA256_CTX ctx;
- sha256_init(&ctx);
- unsigned char chunk[65536];
- size_t rn;
- while ((rn = fread(chunk, 1, sizeof chunk, f)) > 0)
- sha256_update(&ctx, (const BYTE *)chunk, rn);
- fclose(f);
- unsigned char digest[32];
- sha256_final(&ctx, digest);
- char hex[65];
- util_hex(digest, 32, hex);
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
-
- audit_append(r->db, r->org_id, r->sess->user_id, 0, "backup.snapshot",
- "{}", "OK", NULL);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "path", path);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "sha256", hex);
- yyjson_mut_obj_add_int(r->rdoc, o, "size", (int64_t)sb.st_size);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "at", ts);
- return o;
-}
-
-/* ------------------------------------------------------------------ */
-/* ------------------------------------------------------------------ */
-/* shared helpers for M2 commands */
-/* ------------------------------------------------------------------ */
-
-static 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;
-}
-
-static int hash_file(const char *path, char hex[65], int64_t *size)
-{
- FILE *f = fopen(path, "rb");
- if (!f)
- return -1;
- SHA256_CTX ctx;
- sha256_init(&ctx);
- unsigned char chunk[65536];
- size_t rn;
- int64_t total = 0;
- while ((rn = fread(chunk, 1, sizeof chunk, f)) > 0) {
- sha256_update(&ctx, (const BYTE *)chunk, rn);
- total += (int64_t)rn;
- }
- int bad = ferror(f);
- fclose(f);
- if (bad)
- return -1;
- unsigned char digest[32];
- sha256_final(&ctx, digest);
- util_hex(digest, 32, hex);
- if (size)
- *size = total;
- return 0;
-}
-
-static int is_digits(const char *s)
-{
- if (!s || !*s)
- return 0;
- for (; *s; s++)
- if (*s < '0' || *s > '9')
- return 0;
- return 1;
-}
-
-/* ------------------------------------------------------------------ */
-/* 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);
-}
-
-/* ------------------------------------------------------------------ */
-/* fiscal years and period locks */
-/* ------------------------------------------------------------------ */
-
-static yyjson_mut_val *fy_json(struct req *r, sqlite3_stmt *st)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "label",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "start_date",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "end_date",
- sq(sqlite3_column_text(st, 3)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "status",
- sq(sqlite3_column_text(st, 4)));
- if (sqlite3_column_type(st, 5) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "locked_until");
- else
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "locked_until",
- sq(sqlite3_column_text(st, 5)));
- yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore",
- sqlite3_column_int64(st, 6));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "events",
- sq(sqlite3_column_text(st, 7)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "agm_date",
- sq(sqlite3_column_text(st, 8)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "dividend_date",
- sq(sqlite3_column_text(st, 9)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "employees",
- sq(sqlite3_column_text(st, 10)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes",
- sq(sqlite3_column_text(st, 11)));
- return o;
-}
-
-#define FY_COLUMNS \
- "id,label,start_date,end_date,status,locked_until,dividend_ore," \
- "events,agm_date,dividend_date,employees,notes"
-
-static yyjson_mut_val *h_fiscal_year_list(struct req *r)
-{
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT " FY_COLUMNS " FROM fiscal_years WHERE org_id=?1"
- " ORDER BY start_date",
- -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, fy_json(r, 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_fiscal_year_get(struct req *r)
-{
- int64_t id = 0;
- arg_int(r->args, "id", &id);
- sqlite3_stmt *st = NULL;
- const char *sql = id
- ? "SELECT " FY_COLUMNS " FROM fiscal_years"
- " WHERE org_id=?1 AND id=?2"
- : "SELECT " FY_COLUMNS " FROM fiscal_years"
- " WHERE org_id=?1 ORDER BY start_date DESC LIMIT 1";
- 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);
- yyjson_mut_val *o = NULL;
- if (sqlite3_step(st) == SQLITE_ROW)
- o = fy_json(r, st);
- sqlite3_finalize(st);
- if (!o)
- return fail(r, "NOT_FOUND", "fiscal year not found");
- return o;
-}
-
-static int fy_exists_in_range(sqlite3 *db, int64_t org_id, const char *start,
- const char *end)
-{
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- db,
- "SELECT count(*) FROM fiscal_years WHERE org_id=?1"
- " AND NOT (end_date < ?2 OR start_date > ?3)",
- -1, &st, NULL) != SQLITE_OK)
- return 1;
- sqlite3_bind_int64(st, 1, org_id);
- sqlite3_bind_text(st, 2, start, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 3, end, -1, SQLITE_TRANSIENT);
- int64_t n = 0;
- if (sqlite3_step(st) == SQLITE_ROW)
- n = sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
- return n > 0;
-}
-
-static yyjson_mut_val *h_fiscal_year_open(struct req *r)
-{
- const char *label = arg_str(r->args, "label");
- const char *start = arg_str(r->args, "start_date");
- const char *end = arg_str(r->args, "end_date");
- if (!label || !*label)
- return fail(r, "INVALID_ARGS", "label is required");
- if (!util_parse_iso_date(start) || !util_parse_iso_date(end))
- return fail(r, "INVALID_ARGS", "start_date and end_date must be YYYY-MM-DD");
- if (strcmp(start, end) >= 0)
- return fail(r, "INVALID_ARGS", "start_date must be before end_date");
- if (fy_exists_in_range(r->db, r->org_id, start, end))
- return fail(r, "CONFLICT", "fiscal year overlaps an existing one");
- /* "Information om året" carries over from the latest earlier year, so
- only what changes needs to be edited. Dates and the dividend are
- year-specific and start empty. */
- char prev_events[600] = "", prev_emp[64] = "", prev_notes[600] = "";
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT events,employees,notes FROM fiscal_years"
- " WHERE org_id=?1 AND end_date < ?2"
- " ORDER BY end_date DESC LIMIT 1",
- -1, &st, NULL) == SQLITE_OK) {
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, start, -1, SQLITE_TRANSIENT);
- if (sqlite3_step(st) == SQLITE_ROW) {
- snprintf(prev_events, sizeof prev_events, "%s",
- (const char *)sqlite3_column_text(st, 0));
- snprintf(prev_emp, sizeof prev_emp, "%s",
- (const char *)sqlite3_column_text(st, 1));
- snprintf(prev_notes, sizeof prev_notes, "%s",
- (const char *)sqlite3_column_text(st, 2));
- }
- sqlite3_finalize(st);
- }
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO fiscal_years(org_id,label,start_date,end_date,"
- "events,employees,notes,created_at)"
- " VALUES(?1,?2,?3,?4,?5,?6,?7,?8)",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, label, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 3, start, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 4, end, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 5, prev_events, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 6, prev_emp, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 7, prev_notes, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 8, ts, -1, SQLITE_TRANSIENT);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE)
- return fail(r, "CONFLICT", "could not create fiscal year");
- 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,
- "fiscal_year.open", 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, "label", label);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "start_date", start);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "end_date", end);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "status", "open");
- return o;
-}
-
-static yyjson_mut_val *h_fiscal_year_close(struct req *r)
-{
- int64_t id = 0;
- int confirm = 0;
- arg_int(r->args, "id", &id);
- arg_bool(r->args, "confirm", &confirm);
- if (id <= 0 || !confirm)
- return fail(r, "INVALID_ARGS", "id and confirm:true are required");
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE fiscal_years SET status='closed', closed_at=?3,"
- " closed_by=?4 WHERE org_id=?1 AND id=?2 AND status='open'",
- -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);
- sqlite3_bind_text(st, 3, ts, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 4, r->sess->user_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", "open fiscal year not found");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "fiscal_year.close", reqjson, "OK", NULL);
- free(reqjson);
- return yyjson_mut_obj(r->rdoc);
-}
-
-static yyjson_mut_val *h_fiscal_year_reopen(struct req *r)
-{
- int64_t id = 0;
- int confirm = 0;
- arg_int(r->args, "id", &id);
- arg_bool(r->args, "confirm", &confirm);
- if (id <= 0 || !confirm)
- return fail(r, "INVALID_ARGS", "id and confirm:true are required");
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
- }
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE fiscal_years SET status='open', closed_at=NULL,"
- " closed_by=NULL WHERE org_id=?1 AND id=?2 AND status='closed'",
- -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", "closed fiscal year not found");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "fiscal_year.reopen", reqjson, "OK", NULL);
- free(reqjson);
- return yyjson_mut_obj(r->rdoc);
-}
-
-/* Fiscal-year metadata that is not a posting: the board's proposed
- dividend and the year's material events, kept for the årsredovisning
- draft. Both fields are optional; at least one must be given. */
-static yyjson_mut_val *h_fiscal_year_update(struct req *r)
-{
- int64_t id = 0, dividend = 0;
- if (!arg_int(r->args, "id", &id) || id <= 0)
- return fail(r, "INVALID_ARGS", "id is required");
- int have_div = arg_int(r->args, "dividend_ore", &dividend);
- const char *events = arg_str(r->args, "events");
- const char *agm = arg_str(r->args, "agm_date");
- const char *pay = arg_str(r->args, "dividend_date");
- const char *employees = arg_str(r->args, "employees");
- const char *notes = arg_str(r->args, "notes");
- if (!have_div && !events && !agm && !pay && !employees && !notes)
- return fail(r, "INVALID_ARGS", "nothing to update");
- if (have_div && dividend < 0)
- return fail(r, "INVALID_ARGS", "dividend_ore must be >= 0");
- if (agm && *agm && !util_parse_iso_date(agm))
- return fail(r, "INVALID_ARGS", "agm_date must be YYYY-MM-DD");
- if (pay && *pay && !util_parse_iso_date(pay))
- return fail(r, "INVALID_ARGS", "dividend_date must be YYYY-MM-DD");
- static const char *const skeys[4] = { "events", "agm_date",
- "dividend_date", "employees" };
- const char *svals[4] = { events, agm, pay, employees };
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db,
- "SELECT count(*) FROM fiscal_years"
- " 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);
- int64_t exists = 0;
- if (sqlite3_step(st) == SQLITE_ROW)
- exists = sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
- if (!exists)
- return fail(r, "NOT_FOUND", "fiscal year not found");
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
- if (have_div)
- yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore", dividend);
- for (int i = 0; i < 4; i++)
- if (svals[i])
- yyjson_mut_obj_add_strcpy(r->rdoc, o, skeys[i], svals[i]);
- if (notes)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes", notes);
- return o;
- }
- if (sqlite3_prepare_v2(r->db,
- "UPDATE fiscal_years SET"
- " dividend_ore=CASE WHEN ?3<0 THEN dividend_ore"
- " ELSE ?3 END,"
- " events=COALESCE(?4,events),"
- " agm_date=COALESCE(?5,agm_date),"
- " dividend_date=COALESCE(?6,dividend_date),"
- " employees=COALESCE(?7,employees),"
- " notes=COALESCE(?8,notes)"
- " 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);
- sqlite3_bind_int64(st, 3, have_div ? dividend : -1);
- for (int i = 0; i < 4; i++) {
- if (svals[i])
- sqlite3_bind_text(st, i + 4, svals[i], -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, i + 4);
- }
- if (notes)
- sqlite3_bind_text(st, 8, notes, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 8);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE)
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "fiscal_year.update", reqjson, "OK", NULL);
- free(reqjson);
- if (sqlite3_prepare_v2(r->db,
- "SELECT " FY_COLUMNS " FROM fiscal_years"
- " 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);
- yyjson_mut_val *o = NULL;
- if (sqlite3_step(st) == SQLITE_ROW)
- o = fy_json(r, st);
- sqlite3_finalize(st);
- if (!o)
- return fail(r, "NOT_FOUND", "fiscal year not found");
- return o;
-}
-
-static yyjson_mut_val *h_period_lock(struct req *r)
-{
- int64_t fy = 0;
- arg_int(r->args, "fiscal_year", &fy);
- const char *until = arg_str(r->args, "until");
- if (fy <= 0 || !util_parse_iso_date(until))
- return fail(r, "INVALID_ARGS",
- "fiscal_year and until (YYYY-MM-DD) are required");
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE fiscal_years SET locked_until=?3 WHERE org_id=?1 AND id=?2"
- " AND ?3 BETWEEN start_date AND end_date",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, fy);
- sqlite3_bind_text(st, 3, until, -1, SQLITE_TRANSIENT);
- 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",
- "fiscal year not found or until is outside it");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "period.lock", reqjson, "OK", NULL);
- free(reqjson);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year", fy);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "locked_until", until);
- return o;
-}
-
-static yyjson_mut_val *h_period_unlock(struct req *r)
-{
- int64_t fy = 0;
- arg_int(r->args, "fiscal_year", &fy);
- if (fy <= 0)
- return fail(r, "INVALID_ARGS", "fiscal_year is required");
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE fiscal_years SET locked_until=NULL 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, fy);
- 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", "fiscal year not found");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "period.unlock", reqjson, "OK", NULL);
- free(reqjson);
- return yyjson_mut_obj(r->rdoc);
-}
-
-/* ------------------------------------------------------------------ */
-/* vouchers */
-/* ------------------------------------------------------------------ */
-
-static int parse_rows(struct req *r, yyjson_val *rowsv, struct ledger_row **out,
- size_t *out_n)
-{
- if (!rowsv || !yyjson_is_arr(rowsv))
- return fail(r, "INVALID_ARGS", "rows must be an array") ? -1 : -1;
- size_t n = yyjson_arr_size(rowsv);
- struct ledger_row *rows = xcalloc(n ? n : 1, sizeof *rows);
- size_t k = 0;
- yyjson_arr_iter it = yyjson_arr_iter_with(rowsv);
- yyjson_val *item;
- while ((item = yyjson_arr_iter_next(&it))) {
- if (!yyjson_is_obj(item)) {
- free(rows);
- fail(r, "INVALID_ARGS", "each row must be an object");
- return -1;
- }
- yyjson_val *av = yyjson_obj_get(item, "account");
- if (!av || !yyjson_is_str(av)) {
- free(rows);
- failf(r, "INVALID_ARGS", "row %zu: account is required", k + 1);
- return -1;
- }
- yyjson_val *dv = yyjson_obj_get(item, "debit_ore");
- yyjson_val *cv = yyjson_obj_get(item, "credit_ore");
- if ((dv && !yyjson_is_int(dv)) || (cv && !yyjson_is_int(cv))) {
- free(rows);
- failf(r, "INVALID_ARGS", "row %zu: amounts must be integer öre",
- k + 1);
- return -1;
- }
- rows[k].account = yyjson_get_str(av);
- rows[k].debit_ore = dv ? yyjson_get_int(dv) : 0;
- rows[k].credit_ore = cv ? yyjson_get_int(cv) : 0;
- yyjson_val *d = yyjson_obj_get(item, "description");
- rows[k].description = d && yyjson_is_str(d) ? yyjson_get_str(d) : NULL;
- k++;
- }
- *out = rows;
- *out_n = k;
- return 0;
-}
-
-static int parse_attachment_ids(struct req *r, int64_t **out, size_t *out_n)
-{
- yyjson_val *av = r->args ? yyjson_obj_get(r->args, "attachment_ids") : NULL;
- *out = NULL;
- *out_n = 0;
- if (!av)
- return 0;
- if (!yyjson_is_arr(av))
- return fail(r, "INVALID_ARGS", "attachment_ids must be an array") ? -1
- : -1;
- size_t n = yyjson_arr_size(av);
- int64_t *ids = xcalloc(n ? n : 1, sizeof *ids);
- size_t k = 0;
- yyjson_arr_iter it = yyjson_arr_iter_with(av);
- yyjson_val *item;
- while ((item = yyjson_arr_iter_next(&it))) {
- if (!yyjson_is_int(item)) {
- free(ids);
- fail(r, "INVALID_ARGS", "attachment_ids must be integers");
- return -1;
- }
- ids[k++] = yyjson_get_int(item);
- }
- *out = ids;
- *out_n = k;
- return 0;
-}
-
-/* ------------------------------------------------------------------ */
-/* settings */
-/* ------------------------------------------------------------------ */
-
-static yyjson_mut_val *h_settings_get(struct req *r)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- int have_default = 0, have_bank = 0, have_receivable = 0, have_revenue = 0;
- int have_password = 0, have_security = 0;
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db, "SELECT key,value FROM settings WHERE org_id=?1", -1, &st,
- NULL) == SQLITE_OK) {
- sqlite3_bind_int64(st, 1, r->org_id);
- while (sqlite3_step(st) == SQLITE_ROW) {
- const char *k = (const char *)sqlite3_column_text(st, 0);
- const char *v = (const char *)sqlite3_column_text(st, 1);
- if (k) {
- if (secret_key_is_secret(k)) {
- have_password = 1;
- continue;
- }
- yyjson_mut_obj_add(o, yyjson_mut_strcpy(r->rdoc, k),
- yyjson_mut_strcpy(r->rdoc, v ? v : ""));
- if (strcmp(k, "default_series") == 0)
- have_default = 1;
- if (strcmp(k, "bank_account") == 0)
- have_bank = 1;
- if (strcmp(k, "invoice_receivable_account") == 0)
- have_receivable = 1;
- if (strcmp(k, "invoice_revenue_account") == 0)
- have_revenue = 1;
- if (strcmp(k, "smtp_security") == 0)
- have_security = 1;
- }
- }
- sqlite3_finalize(st);
- }
- if (!have_default)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "default_series", "A");
- if (!have_bank)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "bank_account", "1930");
- if (!have_receivable)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_receivable_account",
- "1510");
- if (!have_revenue)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_revenue_account",
- "3001");
- if (!have_security)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "smtp_security", "starttls");
- yyjson_mut_obj_add_bool(r->rdoc, o, "smtp_password_set", have_password);
- return o;
-}
-
-static yyjson_mut_val *settings_result(struct req *r, const char *key,
- const char *value, int dry)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "key", key);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "value", value);
- if (dry)
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
-}
-
-static char *settings_secret_audit_json(const char *key)
-{
- yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *o = yyjson_mut_obj(doc);
- yyjson_mut_doc_set_root(doc, o);
- yyjson_mut_obj_add_strcpy(doc, o, "key", key);
- yyjson_mut_obj_add_strcpy(doc, o, "value", "[redacted]");
- char *json = yyjson_mut_write(doc, 0, NULL);
- yyjson_mut_doc_free(doc);
- return json ? json : xstrdup("{}");
-}
-
-static yyjson_mut_val *h_settings_set(struct req *r)
-{
- const char *key = arg_str(r->args, "key");
- const char *value = arg_str(r->args, "value");
- if (!key || !value)
- return fail(r, "INVALID_ARGS", "key and value are required");
- if (secret_key_is_secret(key)) {
- if (!secret_available())
- return fail(r, "INTERNAL",
- "BOKFD_SECRET_KEY is missing or invalid");
- char *stored = NULL;
- if (*value && secret_encrypt(value, &stored) != 0)
- return fail(r, "INTERNAL",
- "BOKFD_SECRET_KEY is missing or invalid");
- if (r->dry_run) {
- free(stored);
- return settings_result(r, key, "[redacted]", 1);
- }
- sqlite3_stmt *st = NULL;
- const char *sql = *value
- ? "INSERT INTO settings(org_id,key,value)"
- " VALUES(?1,?2,?3)"
- " ON CONFLICT(org_id,key) DO UPDATE SET"
- " value=excluded.value"
- : "DELETE FROM settings WHERE org_id=?1"
- " AND key=?2";
- if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK) {
- free(stored);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, key, -1, SQLITE_TRANSIENT);
- if (*value)
- sqlite3_bind_text(st, 3, stored, -1, SQLITE_TRANSIENT);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- free(stored);
- if (rc != SQLITE_DONE)
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- char *reqjson = settings_secret_audit_json(key);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "settings.set", reqjson, "OK", NULL);
- free(reqjson);
- return settings_result(r, key, "[redacted]", 0);
- }
- size_t maxlen;
- int digits_only = 0, bankgiro = 0, port = 0, security = 0;
- if (strcmp(key, "default_series") == 0)
- maxlen = 8;
- else if (strcmp(key, "attachment_dir") == 0 ||
- strcmp(key, "smtp_host") == 0 ||
- strcmp(key, "smtp_user") == 0)
- maxlen = 255;
- else if (strcmp(key, "smtp_from") == 0 ||
- strcmp(key, "smtp_reply_to") == 0)
- maxlen = 254;
- else if (strcmp(key, "smtp_port") == 0) {
- maxlen = 5;
- port = 1;
- } else if (strcmp(key, "smtp_security") == 0) {
- maxlen = 8;
- security = 1;
- } else if (strcmp(key, "bank_account") == 0 ||
- strcmp(key, "invoice_receivable_account") == 0 ||
- strcmp(key, "invoice_revenue_account") == 0) {
- maxlen = 10;
- digits_only = 1;
- } else if (strcmp(key, "invoice_bankgiro") == 0) {
- maxlen = 16;
- bankgiro = 1;
- } else if (strcmp(key, "invoice_our_ref") == 0) {
- maxlen = 64;
- } else
- return fail(r, "UNSUPPORTED", "unknown setting");
- size_t len = strlen(value);
- if (len == 0 || len > maxlen)
- return failf(r, "INVALID_ARGS", "%s must be 1-%zu characters", key,
- maxlen);
- for (const char *p = value; *p; p++) {
- if ((digits_only || port) && (*p < '0' || *p > '9'))
- return failf(r, "INVALID_ARGS", "%s must be digits only", key);
- if (bankgiro && !((*p >= '0' && *p <= '9') || *p == '-'))
- return failf(r, "INVALID_ARGS",
- "%s must be digits and hyphens", key);
- if ((unsigned char)*p < 32)
- return failf(r, "INVALID_ARGS",
- "%s must not contain control characters", key);
- }
- if (port) {
- long v = strtol(value, NULL, 10);
- if (v < 1 || v > 65535)
- return failf(r, "INVALID_ARGS", "%s must be 1-65535", key);
- }
- if (security && strcmp(value, "starttls") != 0 &&
- strcmp(value, "tls") != 0 && strcmp(value, "plain") != 0)
- return failf(r, "INVALID_ARGS", "%s must be starttls, tls or plain",
- key);
- if (r->dry_run)
- return settings_result(r, key, value, 1);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO settings(org_id,key,value) VALUES(?1,?2,?3)"
- " ON CONFLICT(org_id,key) DO UPDATE SET value=excluded.value",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, key, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 3, value, -1, SQLITE_TRANSIENT);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE)
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "settings.set", reqjson, "OK", NULL);
- free(reqjson);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "key", key);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "value", value);
- return o;
-}
-
-/* ------------------------------------------------------------------ */
-/* customer register */
-/* ------------------------------------------------------------------ */
-
-#define CUSTOMER_COLUMNS \
- "id,name,address,postal_code,city,country,vat_nr,email,your_ref," \
- "payment_days,notes,active,created_at,COALESCE(updated_at,'')"
-
-static yyjson_mut_val *customer_json(struct req *r, sqlite3_stmt *st)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "address",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "postal_code",
- sq(sqlite3_column_text(st, 3)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "city",
- sq(sqlite3_column_text(st, 4)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "country",
- sq(sqlite3_column_text(st, 5)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "vat_nr",
- sq(sqlite3_column_text(st, 6)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "email",
- sq(sqlite3_column_text(st, 7)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "your_ref",
- sq(sqlite3_column_text(st, 8)));
- yyjson_mut_obj_add_int(r->rdoc, o, "payment_days",
- sqlite3_column_int64(st, 9));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes",
- sq(sqlite3_column_text(st, 10)));
- yyjson_mut_obj_add_bool(r->rdoc, o, "active",
- sqlite3_column_int(st, 11) != 0);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at",
- sq(sqlite3_column_text(st, 12)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "updated_at",
- sq(sqlite3_column_text(st, 13)));
- return o;
-}
-
-struct customer_input {
- const char *name;
- const char *address;
- const char *postal_code;
- const char *city;
- const char *country;
- const char *vat_nr;
- const char *email;
- const char *your_ref;
- const char *notes;
- int64_t payment_days;
- int have_payment;
- int active;
- int have_active;
-};
-
-static void customer_input_read(struct req *r, struct customer_input *in)
-{
- memset(in, 0, sizeof *in);
- in->name = arg_str(r->args, "name");
- in->address = arg_str(r->args, "address");
- in->postal_code = arg_str(r->args, "postal_code");
- in->city = arg_str(r->args, "city");
- in->country = arg_str(r->args, "country");
- in->vat_nr = arg_str(r->args, "vat_nr");
- in->email = arg_str(r->args, "email");
- in->your_ref = arg_str(r->args, "your_ref");
- in->notes = arg_str(r->args, "notes");
- in->have_payment = arg_int(r->args, "payment_days", &in->payment_days);
- in->have_active = arg_bool(r->args, "active", &in->active);
-}
-
-static int customer_input_validate(struct req *r,
- const struct customer_input *in,
- int is_create)
-{
- if (is_create && (!in->name || !*in->name)) {
- fail(r, "INVALID_ARGS", "name is required");
- return -1;
- }
- if (in->name && !*in->name) {
- fail(r, "INVALID_ARGS", "name cannot be empty");
- return -1;
- }
- if (in->name && strlen(in->name) > 200) {
- fail(r, "INVALID_ARGS", "name is too long");
- return -1;
- }
- static const char *const names[] = {
- "address", "postal_code", "city", "country", "vat_nr",
- "email", "your_ref", "notes",
- };
- static const size_t maxlen[] = { 500, 32, 120, 64, 64, 254, 120, 2000 };
- const char *values[] = { in->address, in->postal_code, in->city,
- in->country, in->vat_nr, in->email,
- in->your_ref, in->notes };
- for (size_t i = 0; i < sizeof maxlen / sizeof maxlen[0]; i++) {
- if (values[i] && strlen(values[i]) > maxlen[i]) {
- failf(r, "INVALID_ARGS", "%s is too long", names[i]);
- return -1;
- }
- }
- if (in->have_payment && in->payment_days < 0) {
- fail(r, "INVALID_ARGS", "payment_days must be >= 0");
- return -1;
- }
- return 0;
-}
-
-static yyjson_mut_val *customer_lookup(struct req *r, int64_t id, int *found)
-{
- sqlite3_stmt *st = NULL;
- *found = 0;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT " CUSTOMER_COLUMNS " FROM customers"
- " WHERE org_id=?1 AND id=?2",
- -1, &st, NULL) != SQLITE_OK) {
- *found = -1;
- fail(r, "INTERNAL", "database error");
- return NULL;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, id);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return NULL;
- }
- yyjson_mut_val *o = customer_json(r, st);
- sqlite3_finalize(st);
- *found = 1;
- return o;
-}
-
-static int customer_name_taken(struct req *r, const char *name, int64_t except_id)
-{
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT id FROM customers WHERE org_id=?1 AND name=?2"
- " AND id<>?3",
- -1, &st, NULL) != SQLITE_OK) {
- fail(r, "INTERNAL", "database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 3, except_id);
- int taken = sqlite3_step(st) == SQLITE_ROW;
- sqlite3_finalize(st);
- return taken;
-}
-
-static yyjson_mut_val *h_customer_list(struct req *r)
-{
- int active_only = 0;
- arg_bool(r->args, "active_only", &active_only);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT " CUSTOMER_COLUMNS " FROM customers WHERE org_id=?1"
- " AND (?2=0 OR active=1) ORDER BY name COLLATE NOCASE, id",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int(st, 2, active_only);
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- while (sqlite3_step(st) == SQLITE_ROW)
- yyjson_mut_arr_add_val(items, customer_json(r, st));
- sqlite3_finalize(st);
- yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
- return out;
-}
-
-static yyjson_mut_val *h_customer_get(struct req *r)
-{
- int64_t id = 0;
- if (!arg_int(r->args, "id", &id) || id <= 0)
- return fail(r, "INVALID_ARGS", "id is required");
- int found = 0;
- yyjson_mut_val *o = customer_lookup(r, id, &found);
- if (found < 0)
- return NULL;
- if (!found)
- return fail(r, "NOT_FOUND", "customer not found");
- return o;
-}
-
-static yyjson_mut_val *h_customer_create(struct req *r)
-{
- struct customer_input in;
- customer_input_read(r, &in);
- if (customer_input_validate(r, &in, 1) != 0)
- return NULL;
- if (!in.have_payment)
- in.payment_days = 30;
- const char *address = in.address ? in.address : "";
- const char *postal = in.postal_code ? in.postal_code : "";
- const char *city = in.city ? in.city : "";
- const char *country = in.country ? in.country : "SE";
- const char *vat = in.vat_nr ? in.vat_nr : "";
- const char *email = in.email ? in.email : "";
- const char *your_ref = in.your_ref ? in.your_ref : "";
- const char *notes = in.notes ? in.notes : "";
- int taken = customer_name_taken(r, in.name, 0);
- if (taken < 0)
- return NULL;
- if (taken)
- return fail(r, "CONFLICT", "a customer with this name already exists");
-
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", 0);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", in.name);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "address", address);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "postal_code", postal);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "city", city);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "country", country);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "vat_nr", vat);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "email", email);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "your_ref", your_ref);
- yyjson_mut_obj_add_int(r->rdoc, o, "payment_days", in.payment_days);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes", notes);
- yyjson_mut_obj_add_bool(r->rdoc, o, "active", true);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
- }
-
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO customers(org_id,name,address,postal_code,city,country,"
- "vat_nr,email,your_ref,payment_days,notes,created_at)"
- " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, in.name, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 3, address, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 4, postal, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 5, city, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 6, country, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 7, vat, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 8, email, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 9, your_ref, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 10, in.payment_days);
- sqlite3_bind_text(st, 11, notes, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 12, 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", "a customer with this name 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,
- "customer.create", reqjson, "OK", NULL);
- free(reqjson);
- int found = 0;
- yyjson_mut_val *o = customer_lookup(r, id, &found);
- if (found < 0)
- return NULL;
- if (!found)
- return fail(r, "INTERNAL", "could not read the new customer");
- return o;
-}
-
-static yyjson_mut_val *h_customer_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");
- struct customer_input in;
- customer_input_read(r, &in);
- if (customer_input_validate(r, &in, 0) != 0)
- return NULL;
- if (!in.name && !in.address && !in.postal_code && !in.city && !in.country &&
- !in.vat_nr && !in.email && !in.your_ref && !in.notes &&
- !in.have_payment && !in.have_active)
- return fail(r, "INVALID_ARGS", "nothing to update");
-
- int found = 0;
- yyjson_mut_val *existing = customer_lookup(r, id, &found);
- (void)existing;
- if (found < 0)
- return NULL;
- if (!found)
- return fail(r, "NOT_FOUND", "customer not found");
- if (in.name) {
- int taken = customer_name_taken(r, in.name, id);
- if (taken < 0)
- return NULL;
- if (taken)
- return fail(r, "CONFLICT", "a customer with this name already exists");
- }
-
- if (r->dry_run) {
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT id,COALESCE(?3,name),COALESCE(?4,address),"
- "COALESCE(?5,postal_code),COALESCE(?6,city),"
- "COALESCE(?7,country),COALESCE(?8,vat_nr),"
- "COALESCE(?9,email),COALESCE(?10,your_ref),"
- "CASE WHEN ?11<0 THEN payment_days ELSE ?11 END,"
- "COALESCE(?12,notes),"
- "CASE WHEN ?13<0 THEN active ELSE ?13 END,"
- "created_at,COALESCE(updated_at,'')"
- " FROM customers 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);
- const char *texts[] = { in.name, in.address, in.postal_code, in.city,
- in.country, in.vat_nr, in.email, in.your_ref,
- in.notes };
- static const int bind_index[] = { 3, 4, 5, 6, 7, 8, 9, 10, 12 };
- for (size_t i = 0; i < sizeof texts / sizeof texts[0]; i++) {
- if (texts[i])
- sqlite3_bind_text(st, bind_index[i], texts[i], -1,
- SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, bind_index[i]);
- }
- sqlite3_bind_int64(st, 11, in.have_payment ? in.payment_days : -1);
- sqlite3_bind_int64(st, 13, in.have_active ? in.active : -1);
- yyjson_mut_val *o = NULL;
- if (sqlite3_step(st) == SQLITE_ROW)
- o = customer_json(r, st);
- sqlite3_finalize(st);
- if (!o)
- return fail(r, "NOT_FOUND", "customer not found");
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
- }
-
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE customers SET"
- " name=COALESCE(?3,name), address=COALESCE(?4,address),"
- " postal_code=COALESCE(?5,postal_code), city=COALESCE(?6,city),"
- " country=COALESCE(?7,country), vat_nr=COALESCE(?8,vat_nr),"
- " email=COALESCE(?9,email), your_ref=COALESCE(?10,your_ref),"
- " payment_days=CASE WHEN ?11<0 THEN payment_days ELSE ?11 END,"
- " notes=COALESCE(?12,notes),"
- " active=CASE WHEN ?13<0 THEN active ELSE ?13 END,"
- " updated_at=?14 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);
- const char *texts[] = { in.name, in.address, in.postal_code, in.city,
- in.country, in.vat_nr, in.email, in.your_ref,
- in.notes };
- static const int text_index[] = { 3, 4, 5, 6, 7, 8, 9, 10, 12 };
- for (size_t i = 0; i < sizeof texts / sizeof texts[0]; i++) {
- if (texts[i])
- sqlite3_bind_text(st, text_index[i], texts[i], -1,
- SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, text_index[i]);
- }
- sqlite3_bind_int64(st, 11, in.have_payment ? in.payment_days : -1);
- sqlite3_bind_int64(st, 13, in.have_active ? in.active : -1);
- sqlite3_bind_text(st, 14, 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", "a customer with this name already exists");
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- if (sqlite3_changes(r->db) == 0)
- return fail(r, "NOT_FOUND", "customer not found");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "customer.update", reqjson, "OK", NULL);
- free(reqjson);
- found = 0;
- yyjson_mut_val *o = customer_lookup(r, id, &found);
- if (found < 0)
- return NULL;
- if (!found)
- return fail(r, "INTERNAL", "could not read the customer");
- return o;
-}
-
-static yyjson_mut_val *h_customer_archive(struct req *r)
-{
- int64_t id = 0;
- int active = 0;
- if (!arg_int(r->args, "id", &id) || id <= 0 ||
- !arg_bool(r->args, "active", &active))
- return fail(r, "INVALID_ARGS", "id and active are required");
- int found = 0;
- yyjson_mut_val *existing = customer_lookup(r, id, &found);
- (void)existing;
- if (found < 0)
- return NULL;
- if (!found)
- return fail(r, "NOT_FOUND", "customer 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, "active", active != 0);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
- }
-
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE customers SET active=?3, updated_at=?4"
- " 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);
- sqlite3_bind_int(st, 3, active);
- sqlite3_bind_text(st, 4, ts, -1, SQLITE_TRANSIENT);
- 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", "customer not found");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "customer.archive", 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, "active", active != 0);
- return o;
-}
-
-/* ------------------------------------------------------------------ */
-/* invoice sequence */
-/* ------------------------------------------------------------------ */
-
-static int64_t invoice_next_number(struct req *r)
-{
- sqlite3_stmt *st = NULL;
- int64_t next = 1;
- if (sqlite3_prepare_v2(
- r->db, "SELECT next_number FROM invoice_sequence WHERE org_id=?1",
- -1, &st, NULL) != SQLITE_OK)
- return next;
- sqlite3_bind_int64(st, 1, r->org_id);
- if (sqlite3_step(st) == SQLITE_ROW)
- next = sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
- return next > 0 ? next : 1;
-}
-
-static int invoice_take_number(struct req *r, int64_t *out)
-{
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db, "SELECT next_number FROM invoice_sequence WHERE org_id=?1",
- -1, &st, NULL) != SQLITE_OK) {
- fail(r, "INTERNAL", "database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- int have = sqlite3_step(st) == SQLITE_ROW;
- int64_t number = have ? sqlite3_column_int64(st, 0) : 1;
- sqlite3_finalize(st);
- if (have) {
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE invoice_sequence SET next_number=next_number+1"
- " WHERE org_id=?1",
- -1, &st, NULL) != SQLITE_OK) {
- fail(r, "INTERNAL", "database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- fail(r, "DB_BUSY", sqlite3_errmsg(r->db));
- return -1;
- }
- } else {
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO invoice_sequence(org_id,next_number) VALUES(?1,2)",
- -1, &st, NULL) != SQLITE_OK) {
- fail(r, "INTERNAL", "database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- fail(r, "DB_BUSY", sqlite3_errmsg(r->db));
- return -1;
- }
- }
- *out = number;
- return 0;
-}
-
-static yyjson_mut_val *h_invoice_sequence_get(struct req *r)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "next_number", invoice_next_number(r));
- return o;
-}
-
-static yyjson_mut_val *h_invoice_sequence_set(struct req *r)
-{
- int64_t next = 0;
- if (!arg_int(r->args, "next_number", &next) || next <= 0)
- return fail(r, "INVALID_ARGS",
- "next_number must be a positive integer");
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "next_number", next);
- 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 invoice_sequence(org_id,next_number) VALUES(?1,?2)"
- " ON CONFLICT(org_id) DO UPDATE SET next_number=excluded.next_number",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, next);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE)
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "invoice.sequence_set", reqjson, "OK", NULL);
- free(reqjson);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "next_number", next);
- return o;
-}
-
-/* ------------------------------------------------------------------ */
-/* invoice drafts, rendering and issue */
-/* ------------------------------------------------------------------ */
-
-struct draft_line {
- const char *article_no;
- const char *description;
- int64_t quantity_milli;
- const char *unit;
- int64_t unit_price_ore;
- int64_t amount_ore;
- const char *note;
- const char *vat_code;
- char account[16];
-};
-
-struct invoice_draft {
- int64_t customer_id;
- const char *invoice_date;
- const char *due_date;
- const char *delivery_date;
- const char *your_ref;
- const char *our_ref;
- const char *notes;
- struct draft_line *lines;
- size_t nlines;
-};
-
-static void invoice_draft_free(struct invoice_draft *d)
-{
- free(d->lines);
- d->lines = NULL;
- d->nlines = 0;
-}
-
-static int parse_quantity(const char *s, int64_t *out)
-{
- if (!s || !*s || strlen(s) > 24)
- return -1;
- int64_t whole = 0, frac = 0;
- size_t int_digits = 0, frac_digits = 0;
- int seen_sep = 0;
- for (const char *p = s; *p; p++) {
- char c = *p;
- if (c == ',' || c == '.') {
- if (seen_sep)
- return -1;
- seen_sep = 1;
- continue;
- }
- if (c < '0' || c > '9')
- return -1;
- if (!seen_sep) {
- if (int_digits >= 18)
- return -1;
- whole = whole * 10 + (c - '0');
- int_digits++;
- } else {
- if (frac_digits >= 3)
- return -1;
- frac = frac * 10 + (c - '0');
- frac_digits++;
- }
- }
- if (!int_digits || (seen_sep && !frac_digits))
- return -1;
- for (size_t i = frac_digits; i < 3; i++)
- frac *= 10;
- if (whole > (INT64_MAX - frac) / 1000)
- return -1;
- int64_t v = whole * 1000 + frac;
- if (v <= 0)
- return -1;
- *out = v;
- return 0;
-}
-
-static int draft_line_parse(struct req *r, yyjson_val *item, size_t no,
- const char *default_account, struct draft_line *l,
- int64_t *net_total)
-{
- const char *description = arg_str(item, "description");
- if (!description || !*description) {
- failf(r, "INVALID_ARGS", "row %zu: description is required", no);
- return -1;
- }
- const char *qty = arg_str(item, "quantity");
- int64_t quantity_milli = 0;
- if (parse_quantity(qty, &quantity_milli) != 0) {
- failf(r, "INVALID_ARGS",
- "row %zu: quantity must be a positive decimal with at most 3"
- " decimals",
- no);
- return -1;
- }
- int64_t price = 0;
- if (!arg_int(item, "unit_price_ore", &price) || price < 0) {
- failf(r, "INVALID_ARGS",
- "row %zu: unit_price_ore must be a non-negative integer", no);
- return -1;
- }
- const char *vat = arg_str(item, "vat_code");
- if (!vat || !*vat)
- vat = "25";
- if (strcmp(vat, "25") != 0 && strcmp(vat, "12") != 0 &&
- strcmp(vat, "6") != 0 && strcmp(vat, "0") != 0 &&
- strcmp(vat, "rc") != 0 && strcmp(vat, "eu") != 0) {
- failf(r, "INVALID_ARGS",
- "row %zu: vat_code must be one of 25, 12, 6, 0, rc, eu", no);
- return -1;
- }
- const char *account = arg_str(item, "account");
- if (account && !*account)
- account = NULL;
- if (account) {
- if (!is_digits(account) || strlen(account) > 10) {
- failf(r, "INVALID_ARGS", "row %zu: account must be 1-10 digits",
- no);
- return -1;
- }
- snprintf(l->account, sizeof l->account, "%s", account);
- } else {
- snprintf(l->account, sizeof l->account, "%s", default_account);
- }
- l->article_no = arg_str(item, "article_no");
- l->description = description;
- l->quantity_milli = quantity_milli;
- const char *unit = arg_str(item, "unit");
- l->unit = unit && *unit ? unit : "st";
- l->unit_price_ore = price;
- l->note = arg_str(item, "note");
- l->vat_code = vat;
- int64_t product = 0;
- if (__builtin_mul_overflow(quantity_milli, price, &product) ||
- __builtin_add_overflow(product, (int64_t)500, &product)) {
- failf(r, "INVALID_ARGS", "row %zu: amount overflows", no);
- return -1;
- }
- l->amount_ore = product / 1000;
- if (__builtin_add_overflow(*net_total, l->amount_ore, net_total) ||
- *net_total > INT64_MAX / 100) {
- failf(r, "INVALID_ARGS", "row %zu: invoice total overflows", no);
- return -1;
- }
- return 0;
-}
-
-static int parse_invoice_draft(struct req *r, struct invoice_draft *d)
-{
- memset(d, 0, sizeof *d);
- if (!arg_int(r->args, "customer_id", &d->customer_id) ||
- d->customer_id <= 0) {
- fail(r, "INVALID_ARGS", "customer_id is required");
- return -1;
- }
- d->invoice_date = arg_str(r->args, "invoice_date");
- d->due_date = arg_str(r->args, "due_date");
- d->delivery_date = arg_str(r->args, "delivery_date");
- if (!d->invoice_date || !util_parse_iso_date(d->invoice_date)) {
- fail(r, "INVALID_ARGS", "invoice_date must be YYYY-MM-DD");
- return -1;
- }
- if (!d->due_date || !util_parse_iso_date(d->due_date)) {
- fail(r, "INVALID_ARGS", "due_date must be YYYY-MM-DD");
- return -1;
- }
- if (!d->delivery_date)
- d->delivery_date = "";
- if (*d->delivery_date && !util_parse_iso_date(d->delivery_date)) {
- fail(r, "INVALID_ARGS", "delivery_date must be YYYY-MM-DD");
- return -1;
- }
- d->your_ref = arg_str(r->args, "your_ref");
- d->our_ref = arg_str(r->args, "our_ref");
- d->notes = arg_str(r->args, "notes");
- if (!d->your_ref)
- d->your_ref = "";
- if (!d->our_ref)
- d->our_ref = "";
- if (!d->notes)
- d->notes = "";
-
- yyjson_val *rows = r->args ? yyjson_obj_get(r->args, "rows") : NULL;
- if (!rows || !yyjson_is_arr(rows) || yyjson_arr_size(rows) == 0) {
- fail(r, "INVALID_ARGS", "rows must be a non-empty array");
- return -1;
- }
- char *revenue = db_setting(r->db, r->org_id, "invoice_revenue_account");
- const char *default_account =
- revenue && *revenue ? revenue : "3001";
- size_t n = yyjson_arr_size(rows);
- struct draft_line *lines = xcalloc(n, sizeof *lines);
- size_t k = 0;
- int64_t net_total = 0;
- yyjson_arr_iter it = yyjson_arr_iter_with(rows);
- yyjson_val *item;
- while ((item = yyjson_arr_iter_next(&it))) {
- if (!yyjson_is_obj(item)) {
- failf(r, "INVALID_ARGS", "row %zu: must be an object", k + 1);
- free(lines);
- free(revenue);
- return -1;
- }
- if (draft_line_parse(r, item, k + 1, default_account, &lines[k],
- &net_total) != 0) {
- free(lines);
- free(revenue);
- return -1;
- }
- k++;
- }
- free(revenue);
- if (net_total <= 0) {
- fail(r, "INVALID_ARGS", "invoice total must be greater than zero");
- free(lines);
- return -1;
- }
- d->lines = lines;
- d->nlines = k;
- return 0;
-}
-
-struct invoice_view {
- struct invoice_doc doc;
- struct invoice_line *lines;
- char number_str[32];
- char ocr[40];
- char filename[600];
- char description[600];
- char seller_name[256];
- char seller_address[1024];
- char seller_postal[64];
- char seller_city[128];
- char seller_phone[64];
- char seller_email[256];
- char seller_org_nr[64];
- char seller_vat_nr[64];
- char bankgiro[64];
- char customer_name[256];
- char customer_address[1024];
- char customer_postal[64];
- char customer_city[128];
- char customer_vat_nr[64];
-};
-
-static void invoice_view_free(struct invoice_view *v)
-{
- free(v->lines);
- v->lines = NULL;
-}
-
-static int invoice_view_fill(struct req *r, const struct invoice_draft *d,
- int64_t number, struct invoice_view *v)
-{
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT COALESCE(name,''),COALESCE(address,''),"
- "COALESCE(postal_code,''),COALESCE(city,''),"
- "COALESCE(phone,''),COALESCE(email,''),COALESCE(org_nr,''),"
- "COALESCE(vat_nr,'') FROM orgs WHERE id=?1",
- -1, &st, NULL) != SQLITE_OK) {
- fail(r, "INTERNAL", "database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- int have_org = sqlite3_step(st) == SQLITE_ROW;
- if (have_org) {
- snprintf(v->seller_name, sizeof v->seller_name, "%s",
- sq(sqlite3_column_text(st, 0)));
- snprintf(v->seller_address, sizeof v->seller_address, "%s",
- sq(sqlite3_column_text(st, 1)));
- snprintf(v->seller_postal, sizeof v->seller_postal, "%s",
- sq(sqlite3_column_text(st, 2)));
- snprintf(v->seller_city, sizeof v->seller_city, "%s",
- sq(sqlite3_column_text(st, 3)));
- snprintf(v->seller_phone, sizeof v->seller_phone, "%s",
- sq(sqlite3_column_text(st, 4)));
- snprintf(v->seller_email, sizeof v->seller_email, "%s",
- sq(sqlite3_column_text(st, 5)));
- snprintf(v->seller_org_nr, sizeof v->seller_org_nr, "%s",
- sq(sqlite3_column_text(st, 6)));
- snprintf(v->seller_vat_nr, sizeof v->seller_vat_nr, "%s",
- sq(sqlite3_column_text(st, 7)));
- }
- sqlite3_finalize(st);
- if (!have_org) {
- fail(r, "NOT_FOUND", "org not found");
- return -1;
- }
-
- int64_t payment_days = 30;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT name,address,postal_code,city,vat_nr,payment_days,active"
- " FROM customers WHERE org_id=?1 AND id=?2",
- -1, &st, NULL) != SQLITE_OK) {
- fail(r, "INTERNAL", "database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, d->customer_id);
- if (sqlite3_step(st) != SQLITE_ROW || !sqlite3_column_int(st, 6)) {
- sqlite3_finalize(st);
- fail(r, "NOT_FOUND", "customer not found");
- return -1;
- }
- snprintf(v->customer_name, sizeof v->customer_name, "%s",
- sq(sqlite3_column_text(st, 0)));
- snprintf(v->customer_address, sizeof v->customer_address, "%s",
- sq(sqlite3_column_text(st, 1)));
- snprintf(v->customer_postal, sizeof v->customer_postal, "%s",
- sq(sqlite3_column_text(st, 2)));
- snprintf(v->customer_city, sizeof v->customer_city, "%s",
- sq(sqlite3_column_text(st, 3)));
- snprintf(v->customer_vat_nr, sizeof v->customer_vat_nr, "%s",
- sq(sqlite3_column_text(st, 4)));
- payment_days = sqlite3_column_int64(st, 5);
- sqlite3_finalize(st);
-
- char *bankgiro = db_setting(r->db, r->org_id, "invoice_bankgiro");
- snprintf(v->bankgiro, sizeof v->bankgiro, "%s",
- bankgiro && *bankgiro ? bankgiro : "");
- free(bankgiro);
-
- v->lines = xcalloc(d->nlines, sizeof *v->lines);
- for (size_t i = 0; i < d->nlines; i++) {
- v->lines[i].article_no = d->lines[i].article_no;
- v->lines[i].description = d->lines[i].description;
- v->lines[i].quantity_milli = d->lines[i].quantity_milli;
- v->lines[i].unit = d->lines[i].unit;
- v->lines[i].unit_price_ore = d->lines[i].unit_price_ore;
- v->lines[i].amount_ore = d->lines[i].amount_ore;
- v->lines[i].note = d->lines[i].note;
- v->lines[i].vat_code = d->lines[i].vat_code;
- }
-
- v->doc.seller.name = v->seller_name;
- v->doc.seller.address = v->seller_address;
- v->doc.seller.postal_code = v->seller_postal;
- v->doc.seller.city = v->seller_city;
- v->doc.seller.phone = v->seller_phone;
- v->doc.seller.email = v->seller_email;
- v->doc.seller.org_nr = v->seller_org_nr;
- v->doc.seller.vat_nr = v->seller_vat_nr;
- v->doc.seller.bankgiro = v->bankgiro;
- v->doc.customer.name = v->customer_name;
- v->doc.customer.address = v->customer_address;
- v->doc.customer.postal_code = v->customer_postal;
- v->doc.customer.city = v->customer_city;
- v->doc.customer.vat_nr = v->customer_vat_nr;
- v->doc.number = number;
- v->doc.ocr = v->ocr;
- v->doc.invoice_date = d->invoice_date;
- v->doc.due_date = d->due_date;
- v->doc.delivery_date = d->delivery_date;
- v->doc.our_ref = d->our_ref;
- v->doc.your_ref = d->your_ref;
- v->doc.notes = d->notes;
- v->doc.payment_days = (int)payment_days;
- v->doc.lines = v->lines;
- v->doc.nlines = d->nlines;
-
- snprintf(v->number_str, sizeof v->number_str, "%lld", (long long)number);
- int check = invoice_ocr_check(v->number_str);
- snprintf(v->ocr, sizeof v->ocr, "%s%c", v->number_str,
- (char)('0' + (check > 0 ? check : 0)));
- char safe_name[256];
- snprintf(safe_name, sizeof safe_name, "%s", v->customer_name);
- for (char *p = safe_name; *p; p++)
- if (*p == '/')
- *p = '-';
- snprintf(v->filename, sizeof v->filename, "Faktura %lld %s.pdf",
- (long long)number, safe_name);
- snprintf(v->description, sizeof v->description, "Faktura %lld %s",
- (long long)number, safe_name);
- return 0;
-}
-
-/* Mirrors invoice.c vat_part(): round half up per rate base. */
-static int64_t invoice_vat_part(int64_t net, int64_t rate)
-{
- int64_t v = net * rate;
- if (v >= 0)
- return (v + 50) / 100;
- return -((-v + 50) / 100);
-}
-
-static int invoice_build_pdf(struct req *r, struct invoice_view *v,
- struct invoice_totals *t, unsigned char **out,
- size_t *out_len)
-{
- invoice_totals(&v->doc, t);
- *out = NULL;
- *out_len = 0;
- if (invoice_render_pdf(&v->doc, out, out_len) != 0 || !*out) {
- free(*out);
- *out = NULL;
- fail(r, "TOO_LARGE", "invoice does not fit on one page");
- return -1;
- }
- return 0;
-}
-
-static int invoice_prepare(struct req *r, struct invoice_draft *d,
- int64_t number, struct invoice_view *v,
- struct invoice_totals *t, unsigned char **pdf,
- size_t *pdf_len)
-{
- memset(v, 0, sizeof *v);
- if (invoice_view_fill(r, d, number, v) != 0)
- return -1;
- if (invoice_build_pdf(r, v, t, pdf, pdf_len) != 0) {
- invoice_view_free(v);
- return -1;
- }
- return 0;
-}
-
-static yyjson_mut_val *h_invoice_preview(struct req *r)
-{
- struct invoice_draft d;
- if (parse_invoice_draft(r, &d) != 0)
- return NULL;
- int64_t number = invoice_next_number(r);
- struct invoice_view v;
- struct invoice_totals t;
- unsigned char *pdf = NULL;
- size_t pdf_len = 0;
- int rc = invoice_prepare(r, &d, number, &v, &t, &pdf, &pdf_len);
- invoice_draft_free(&d);
- if (rc != 0)
- return NULL;
- char *b64 = util_b64(pdf, pdf_len);
- free(pdf);
- if (!b64) {
- invoice_view_free(&v);
- return fail(r, "INTERNAL", "could not encode the PDF");
- }
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "content_base64", b64);
- yyjson_mut_obj_add_int(r->rdoc, o, "number", number);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "ocr", v.ocr);
- yyjson_mut_obj_add_int(r->rdoc, o, "net_ore", t.net_ore);
- yyjson_mut_obj_add_int(r->rdoc, o, "vat_ore", t.vat_ore);
- yyjson_mut_obj_add_int(r->rdoc, o, "total_ore", t.total_ore);
- free(b64);
- invoice_view_free(&v);
- return o;
-}
-
-static int invoice_store_attachment(struct req *r, const struct invoice_view *v,
- const unsigned char *pdf, size_t pdf_len,
- int64_t *out_id)
-{
- unsigned char hash[32];
- util_sha256(pdf, pdf_len, hash);
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO attachments(org_id,sha256,filename,mime,size_bytes,"
- "content,created_at,created_by)"
- " VALUES(?1,?2,?3,'application/pdf',?4,?5,?6,?7)",
- -1, &st, NULL) != SQLITE_OK) {
- fail(r, "INTERNAL", "database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_blob(st, 2, hash, 32, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 3, v->filename, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 4, (int64_t)pdf_len);
- sqlite3_bind_blob(st, 5, pdf, (int)pdf_len, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 6, ts, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 7, r->sess->user_id);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- fail(r, "DB_BUSY", sqlite3_errmsg(r->db));
- return -1;
- }
- *out_id = db_last_id(r->db);
- return 0;
-}
-
-static int invoice_store_invoice(struct req *r, const struct invoice_draft *d,
- const struct invoice_totals *t, int64_t number,
- int64_t document_id, int64_t voucher_id,
- int64_t *out_id)
-{
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO invoices(org_id,customer_id,number,ocr,invoice_date,"
- "due_date,delivery_date,your_ref,our_ref,notes,net_ore,vat_ore,"
- "total_ore,document_id,voucher_id,created_at,created_by)"
- " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,"
- "?17)",
- -1, &st, NULL) != SQLITE_OK) {
- fail(r, "INTERNAL", "database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, d->customer_id);
- sqlite3_bind_int64(st, 3, number);
- char number_str[32];
- snprintf(number_str, sizeof number_str, "%lld", (long long)number);
- int check = invoice_ocr_check(number_str);
- char ocr[40];
- snprintf(ocr, sizeof ocr, "%s%c", number_str,
- (char)('0' + (check > 0 ? check : 0)));
- sqlite3_bind_text(st, 4, ocr, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 5, d->invoice_date, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 6, d->due_date, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 7, d->delivery_date, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 8, d->your_ref, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 9, d->our_ref, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 10, d->notes, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 11, t->net_ore);
- sqlite3_bind_int64(st, 12, t->vat_ore);
- sqlite3_bind_int64(st, 13, t->total_ore);
- sqlite3_bind_int64(st, 14, document_id);
- sqlite3_bind_int64(st, 15, voucher_id);
- sqlite3_bind_text(st, 16, ts, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 17, r->sess->user_id);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- if ((rc & 0xff) == SQLITE_CONSTRAINT) {
- fail(r, "CONFLICT", "invoice number already exists");
- return -1;
- }
- fail(r, "DB_BUSY", sqlite3_errmsg(r->db));
- return -1;
- }
- int64_t id = db_last_id(r->db);
- for (size_t i = 0; i < d->nlines; i++) {
- const struct draft_line *l = &d->lines[i];
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO invoice_rows(org_id,invoice_id,line_no,article_no,"
- "description,quantity_milli,unit,unit_price_ore,amount_ore,note,"
- "vat_code,account)"
- " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
- -1, &st, NULL) != SQLITE_OK) {
- fail(r, "INTERNAL", "database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, id);
- sqlite3_bind_int64(st, 3, (int64_t)i + 1);
- sqlite3_bind_text(st, 4, l->article_no ? l->article_no : "", -1,
- SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 5, l->description, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 6, l->quantity_milli);
- sqlite3_bind_text(st, 7, l->unit, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 8, l->unit_price_ore);
- sqlite3_bind_int64(st, 9, l->amount_ore);
- sqlite3_bind_text(st, 10, l->note ? l->note : "", -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 11, l->vat_code, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 12, l->account, -1, SQLITE_TRANSIENT);
- rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- fail(r, "DB_BUSY", sqlite3_errmsg(r->db));
- return -1;
- }
- }
- *out_id = id;
- return 0;
-}
-
-static yyjson_mut_val *invoice_issue_result(struct req *r, int dry_run,
- int64_t id, int64_t number,
- const char *ocr, int64_t document_id,
- int64_t voucher_id,
- const struct invoice_totals *t)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- if (dry_run) {
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", 0);
- } else {
- yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
- }
- yyjson_mut_obj_add_int(r->rdoc, o, "number", number);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "ocr", ocr);
- if (dry_run) {
- yyjson_mut_obj_add_null(r->rdoc, o, "document_id");
- yyjson_mut_obj_add_null(r->rdoc, o, "voucher_id");
- } else {
- yyjson_mut_obj_add_int(r->rdoc, o, "document_id", document_id);
- yyjson_mut_obj_add_int(r->rdoc, o, "voucher_id", voucher_id);
- }
- yyjson_mut_obj_add_int(r->rdoc, o, "net_ore", t->net_ore);
- yyjson_mut_obj_add_int(r->rdoc, o, "vat_ore", t->vat_ore);
- yyjson_mut_obj_add_int(r->rdoc, o, "total_ore", t->total_ore);
- return o;
-}
-
-static yyjson_mut_val *h_invoice_issue(struct req *r)
-{
- struct invoice_draft d;
- if (parse_invoice_draft(r, &d) != 0)
- return NULL;
- char *rec_setting =
- db_setting(r->db, r->org_id, "invoice_receivable_account");
- char receivable[16];
- snprintf(receivable, sizeof receivable, "%s",
- rec_setting && *rec_setting ? rec_setting : "1510");
- free(rec_setting);
-
- yyjson_mut_val *res = NULL;
- struct invoice_view v;
- struct invoice_totals t;
- memset(&v, 0, sizeof v);
- memset(&t, 0, sizeof t);
- unsigned char *pdf = NULL;
- size_t pdf_len = 0;
- char *voucher_json = NULL;
- struct ledger_row *vrows = NULL;
- int64_t number = 0, attachment_id = 0, voucher_id = 0, invoice_id = 0;
- int in_tx = 0;
-
- if (db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0) {
- invoice_draft_free(&d);
- return fail(r, "DB_BUSY", "could not start transaction");
- }
- in_tx = 1;
-
- if (r->dry_run)
- number = invoice_next_number(r);
- else if (invoice_take_number(r, &number) != 0)
- goto done;
-
- if (invoice_prepare(r, &d, number, &v, &t, &pdf, &pdf_len) != 0)
- goto done;
-
- size_t cap = 1 + 3 + d.nlines;
- vrows = xcalloc(cap, sizeof *vrows);
- size_t vn = 0;
- vrows[vn].account = receivable;
- vrows[vn].debit_ore = t.total_ore;
- vrows[vn].description = NULL;
- vn++;
- struct {
- int rate;
- const char *account;
- int64_t net;
- } legs[3] = {
- { 25, "2610", t.net_25 },
- { 12, "2620", t.net_12 },
- { 6, "2630", t.net_6 },
- };
- int64_t vat_sum = 0;
- for (size_t i = 0; i < 3; i++) {
- int64_t vat = invoice_vat_part(legs[i].net, legs[i].rate);
- if (vat <= 0)
- continue;
- vrows[vn].account = legs[i].account;
- vrows[vn].credit_ore = vat;
- if (i == 0)
- vrows[vn].description = "Moms 25%";
- else if (i == 1)
- vrows[vn].description = "Moms 12%";
- else
- vrows[vn].description = "Moms 6%";
- vat_sum += vat;
- vn++;
- }
- for (size_t i = 0; i < d.nlines; i++) {
- if (d.lines[i].amount_ore <= 0)
- continue;
- vrows[vn].account = d.lines[i].account;
- vrows[vn].credit_ore = d.lines[i].amount_ore;
- vrows[vn].description = d.lines[i].description;
- vn++;
- }
- int64_t sum_debit = 0, sum_credit = 0;
- for (size_t i = 0; i < vn; i++) {
- sum_debit += vrows[i].debit_ore;
- sum_credit += vrows[i].credit_ore;
- }
- if (vat_sum != t.vat_ore || sum_debit != t.total_ore ||
- sum_debit != sum_credit) {
- fail(r, "INTERNAL", "invoice totals do not match the voucher");
- goto done;
- }
-
- if (!r->dry_run &&
- invoice_store_attachment(r, &v, pdf, pdf_len, &attachment_id) != 0)
- goto done;
-
- struct ledger_post_opts o;
- memset(&o, 0, sizeof o);
- o.org_id = r->org_id;
- o.user_id = r->sess->user_id;
- o.token_id = r->sess->token_id;
- o.date = d.invoice_date;
- o.description = v.description;
- o.rows = vrows;
- o.nrows = vn;
- o.source = "invoice";
- o.dry_run = r->dry_run;
- o.already_in_tx = 1;
- struct ledger_error e;
- if (ledger_post(r->db, &o, &e, &voucher_json) != 0) {
- fail(r, e.code ? e.code : "INTERNAL", e.msg);
- goto done;
- }
-
- if (!r->dry_run) {
- if (!voucher_json) {
- fail(r, "INTERNAL", "empty voucher result");
- goto done;
- }
- yyjson_doc *vd = yyjson_read(voucher_json, strlen(voucher_json), 0);
- if (vd) {
- yyjson_val *idv = yyjson_obj_get(yyjson_doc_get_root(vd), "id");
- if (idv && yyjson_is_int(idv))
- voucher_id = yyjson_get_int(idv);
- yyjson_doc_free(vd);
- }
- if (voucher_id <= 0) {
- fail(r, "INTERNAL", "voucher id missing from the posting");
- goto done;
- }
- if (invoice_store_invoice(r, &d, &t, number, attachment_id, voucher_id,
- &invoice_id) != 0)
- goto done;
- if (sqlite3_exec(r->db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) {
- fail(r, "DB_BUSY", "commit failed");
- goto done;
- }
- in_tx = 0;
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "invoice.issue", reqjson, "OK", NULL);
- free(reqjson);
- }
-
- res = invoice_issue_result(r, r->dry_run, invoice_id, number, v.ocr,
- attachment_id, voucher_id, &t);
-
-done:
- if (in_tx)
- sqlite3_exec(r->db, "ROLLBACK", NULL, NULL, NULL);
- free(pdf);
- free(voucher_json);
- free(vrows);
- invoice_draft_free(&d);
- invoice_view_free(&v);
- return res;
-}
-
-static yyjson_mut_val *invoice_row_json(struct req *r, sqlite3_stmt *st)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "line_no",
- sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "article_no",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_int(r->rdoc, o, "quantity_milli",
- sqlite3_column_int64(st, 3));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "unit",
- sq(sqlite3_column_text(st, 4)));
- yyjson_mut_obj_add_int(r->rdoc, o, "unit_price_ore",
- sqlite3_column_int64(st, 5));
- yyjson_mut_obj_add_int(r->rdoc, o, "amount_ore",
- sqlite3_column_int64(st, 6));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "note",
- sq(sqlite3_column_text(st, 7)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "vat_code",
- sq(sqlite3_column_text(st, 8)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "account",
- sq(sqlite3_column_text(st, 9)));
- return o;
-}
-
-#define INVOICE_ROW_COLUMNS \
- "line_no,article_no,description,quantity_milli,unit,unit_price_ore," \
- "amount_ore,note,vat_code,account"
-
-static yyjson_mut_val *h_invoice_get(struct req *r)
-{
- int64_t id = 0;
- if (!arg_int(r->args, "id", &id) || id <= 0)
- return fail(r, "INVALID_ARGS", "id is required");
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT i.id,i.customer_id,c.name,i.number,i.ocr,i.invoice_date,"
- "i.due_date,i.delivery_date,i.your_ref,i.our_ref,i.notes,i.net_ore,"
- "i.vat_ore,i.total_ore,i.status,i.document_id,i.voucher_id,"
- "i.last_sent_at,i.last_sent_to,i.created_at,i.created_by"
- " FROM invoices i JOIN customers c"
- " ON c.org_id=i.org_id AND c.id=i.customer_id"
- " WHERE i.org_id=?1 AND i.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);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "invoice not found");
- }
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_int(r->rdoc, o, "customer_id",
- sqlite3_column_int64(st, 1));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "customer_name",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_int(r->rdoc, o, "number", sqlite3_column_int64(st, 3));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "ocr",
- sq(sqlite3_column_text(st, 4)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_date",
- sq(sqlite3_column_text(st, 5)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "due_date",
- sq(sqlite3_column_text(st, 6)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "delivery_date",
- sq(sqlite3_column_text(st, 7)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "your_ref",
- sq(sqlite3_column_text(st, 8)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "our_ref",
- sq(sqlite3_column_text(st, 9)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes",
- sq(sqlite3_column_text(st, 10)));
- yyjson_mut_obj_add_int(r->rdoc, o, "net_ore",
- sqlite3_column_int64(st, 11));
- yyjson_mut_obj_add_int(r->rdoc, o, "vat_ore",
- sqlite3_column_int64(st, 12));
- yyjson_mut_obj_add_int(r->rdoc, o, "total_ore",
- sqlite3_column_int64(st, 13));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "status",
- sq(sqlite3_column_text(st, 14)));
- if (sqlite3_column_type(st, 15) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "document_id");
- else
- yyjson_mut_obj_add_int(r->rdoc, o, "document_id",
- sqlite3_column_int64(st, 15));
- if (sqlite3_column_type(st, 16) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "voucher_id");
- else
- yyjson_mut_obj_add_int(r->rdoc, o, "voucher_id",
- sqlite3_column_int64(st, 16));
- if (sqlite3_column_type(st, 17) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "last_sent_at");
- else
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "last_sent_at",
- sq(sqlite3_column_text(st, 17)));
- if (sqlite3_column_type(st, 18) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "last_sent_to");
- else
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "last_sent_to",
- sq(sqlite3_column_text(st, 18)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at",
- sq(sqlite3_column_text(st, 19)));
- yyjson_mut_obj_add_int(r->rdoc, o, "created_by",
- sqlite3_column_int64(st, 20));
- sqlite3_finalize(st);
-
- yyjson_mut_val *rows = yyjson_mut_arr(r->rdoc);
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT " INVOICE_ROW_COLUMNS " FROM invoice_rows"
- " WHERE org_id=?1 AND invoice_id=?2 ORDER BY line_no",
- -1, &st, NULL) == SQLITE_OK) {
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, id);
- while (sqlite3_step(st) == SQLITE_ROW)
- yyjson_mut_arr_add_val(rows, invoice_row_json(r, st));
- sqlite3_finalize(st);
- }
- yyjson_mut_obj_add_val(r->rdoc, o, "rows", rows);
- return o;
-}
-
-static yyjson_mut_val *h_invoice_list(struct req *r)
-{
- int64_t customer_id = 0, limit = 200;
- arg_int(r->args, "customer_id", &customer_id);
- arg_int(r->args, "limit", &limit);
- const char *status = arg_str(r->args, "status");
- if (!status)
- status = "";
- if (limit < 1)
- limit = 200;
- if (limit > 1000)
- limit = 1000;
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT i.id,i.number,i.ocr,i.customer_id,c.name,i.invoice_date,"
- "i.due_date,i.total_ore,i.status,i.document_id,i.voucher_id,"
- "i.last_sent_at,i.last_sent_to"
- " FROM invoices i JOIN customers c"
- " ON c.org_id=i.org_id AND c.id=i.customer_id"
- " WHERE i.org_id=?1"
- " AND (?2=0 OR i.customer_id=?2)"
- " AND (?3='' OR i.status=?3)"
- " ORDER BY i.number DESC, i.id DESC LIMIT ?4",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, customer_id);
- sqlite3_bind_text(st, 3, status, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 4, limit);
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- while (sqlite3_step(st) == SQLITE_ROW) {
- yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_int(r->rdoc, o, "number",
- sqlite3_column_int64(st, 1));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "ocr",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_int(r->rdoc, o, "customer_id",
- sqlite3_column_int64(st, 3));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "customer_name",
- sq(sqlite3_column_text(st, 4)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_date",
- sq(sqlite3_column_text(st, 5)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "due_date",
- sq(sqlite3_column_text(st, 6)));
- yyjson_mut_obj_add_int(r->rdoc, o, "total_ore",
- sqlite3_column_int64(st, 7));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "status",
- sq(sqlite3_column_text(st, 8)));
- if (sqlite3_column_type(st, 9) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "document_id");
- else
- yyjson_mut_obj_add_int(r->rdoc, o, "document_id",
- sqlite3_column_int64(st, 9));
- if (sqlite3_column_type(st, 10) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "voucher_id");
- else
- yyjson_mut_obj_add_int(r->rdoc, o, "voucher_id",
- sqlite3_column_int64(st, 10));
- if (sqlite3_column_type(st, 11) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "last_sent_at");
- else
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "last_sent_at",
- sq(sqlite3_column_text(st, 11)));
- if (sqlite3_column_type(st, 12) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "last_sent_to");
- else
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "last_sent_to",
- sq(sqlite3_column_text(st, 12)));
- }
- sqlite3_finalize(st);
- yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
- return out;
-}
-
-static yyjson_mut_val *h_invoice_pdf(struct req *r)
-{
- int64_t id = 0;
- if (!arg_int(r->args, "id", &id) || id <= 0)
- return fail(r, "INVALID_ARGS", "id is required");
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT document_id FROM invoices 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);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "invoice not found");
- }
- int64_t document_id = sqlite3_column_type(st, 0) == SQLITE_NULL
- ? 0
- : sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
- if (document_id <= 0)
- return fail(r, "NOT_FOUND", "invoice has no stored PDF");
- const void *content = NULL;
- size_t len = 0;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT content FROM attachments 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, document_id);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "invoice PDF not found");
- }
- content = sqlite3_column_blob(st, 0);
- len = (size_t)sqlite3_column_bytes(st, 0);
- char *b64 = util_b64(content ? content : (const unsigned char *)"", len);
- sqlite3_finalize(st);
- if (!b64)
- return fail(r, "INTERNAL", "could not encode the PDF");
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "content_base64", b64);
- free(b64);
- return o;
-}
-
-/* Whole kronor with a space as thousands separator, e.g. 91500 -> "91 500". */
-static void invoice_send_amount(int64_t ore, char *out, size_t n)
-{
- char digits[24];
- snprintf(digits, sizeof digits, "%lld", (long long)((ore + 50) / 100));
- size_t len = strlen(digits);
- size_t o = 0;
- for (size_t i = 0; i < len && o + 1 < n; i++) {
- if (i > 0 && (len - i) % 3 == 0)
- out[o++] = ' ';
- out[o++] = digits[i];
- }
- out[o] = '\0';
-}
-
-static yyjson_mut_val *h_invoice_send(struct req *r)
-{
- int64_t id = 0;
- if (!arg_int(r->args, "id", &id) || id <= 0)
- return fail(r, "INVALID_ARGS", "id is required");
-
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT i.number,i.ocr,i.due_date,i.document_id,i.total_ore,"
- "c.name,COALESCE(c.email,''),COALESCE(o.name,'')"
- " FROM invoices i"
- " JOIN customers c ON c.org_id=i.org_id AND c.id=i.customer_id"
- " JOIN orgs o ON o.id=i.org_id"
- " WHERE i.org_id=?1 AND i.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);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "invoice not found");
- }
- int64_t number = sqlite3_column_int64(st, 0);
- int64_t document_id = sqlite3_column_type(st, 3) == SQLITE_NULL
- ? 0
- : sqlite3_column_int64(st, 3);
- int64_t total_ore = sqlite3_column_int64(st, 4);
- char ocr[40], due_date[16], customer_name[256], customer_email[256];
- char org_name[256];
- snprintf(ocr, sizeof ocr, "%s", sq(sqlite3_column_text(st, 1)));
- snprintf(due_date, sizeof due_date, "%s", sq(sqlite3_column_text(st, 2)));
- snprintf(customer_name, sizeof customer_name, "%s",
- sq(sqlite3_column_text(st, 5)));
- snprintf(customer_email, sizeof customer_email, "%s",
- sq(sqlite3_column_text(st, 6)));
- snprintf(org_name, sizeof org_name, "%s", sq(sqlite3_column_text(st, 7)));
- sqlite3_finalize(st);
- st = NULL;
-
- if (document_id <= 0)
- return fail(r, "NOT_FOUND", "invoice has no stored PDF");
- if (sqlite3_prepare_v2(
- r->db, "SELECT content FROM attachments 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, document_id);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "invoice PDF not found");
- }
- const void *blob = sqlite3_column_blob(st, 0);
- size_t pdf_len = (size_t)sqlite3_column_bytes(st, 0);
- unsigned char *pdf = xmalloc(pdf_len ? pdf_len : 1);
- if (blob && pdf_len)
- memcpy(pdf, blob, pdf_len);
- sqlite3_finalize(st);
- st = NULL;
-
- const char *to = arg_str(r->args, "to");
- if (!to || !*to)
- to = customer_email;
- if (!*to) {
- free(pdf);
- return fail(r, "INVALID_ARGS", "customer has no e-mail address");
- }
-
- char *smtp_host = db_setting(r->db, r->org_id, "smtp_host");
- char *smtp_port = db_setting(r->db, r->org_id, "smtp_port");
- char *smtp_user = db_setting(r->db, r->org_id, "smtp_user");
- char *smtp_from = db_setting(r->db, r->org_id, "smtp_from");
- char *smtp_security = db_setting(r->db, r->org_id, "smtp_security");
- char *smtp_password = db_setting(r->db, r->org_id, "smtp_password");
- char *password = NULL;
- char *body = NULL;
- yyjson_mut_val *res = NULL;
-
- if (!smtp_host || !*smtp_host || !smtp_from || !*smtp_from) {
- fail(r, "SMTP_NOT_CONFIGURED", "smtp_host and smtp_from must be set");
- goto done;
- }
- const char *user = smtp_user && *smtp_user ? smtp_user : "";
- if (*user) {
- if (!smtp_password || !*smtp_password) {
- fail(r, "SMTP_NOT_CONFIGURED",
- "smtp_user is set but smtp_password is missing");
- goto done;
- }
- if (!secret_available()) {
- fail(r, "SMTP_NOT_CONFIGURED",
- "smtp_password is set but BOKFD_SECRET_KEY is missing or"
- " invalid");
- goto done;
- }
- if (secret_decrypt(smtp_password, &password) != 0 || !password) {
- fail(r, "SMTP_NOT_CONFIGURED",
- "cannot decrypt smtp_password, check BOKFD_SECRET_KEY");
- goto done;
- }
- }
- int port = 587;
- if (smtp_port && *smtp_port) {
- long v = strtol(smtp_port, NULL, 10);
- if (v >= 1 && v <= 65535)
- port = (int)v;
- }
- const char *security =
- smtp_security && *smtp_security ? smtp_security : "starttls";
-
- char subject[64];
- snprintf(subject, sizeof subject, "Faktura %lld", (long long)number);
- char amount[32];
- invoice_send_amount(total_ore, amount, sizeof amount);
- size_t body_len = strlen(org_name) + 320;
- body = xmalloc(body_len);
- snprintf(body, body_len,
- "Hej,\n\nBifogat finner du faktura %lld på %s kr med"
- " förfallodatum %s.\nAnge OCR %s vid betalning.\n\n"
- "Med vänlig hälsning\n%s\n",
- (long long)number, amount, due_date, ocr, org_name);
-
- char safe_name[256];
- snprintf(safe_name, sizeof safe_name, "%s", customer_name);
- for (char *p = safe_name; *p; p++)
- if (*p == '/')
- *p = '-';
- char attach_name[320];
- snprintf(attach_name, sizeof attach_name, "Faktura %lld %s.pdf",
- (long long)number, safe_name);
-
- 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_strcpy(r->rdoc, o, "to", to);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "subject", subject);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- res = o;
- goto done;
- }
-
- struct smtp_message m;
- memset(&m, 0, sizeof m);
- m.host = smtp_host;
- m.port = port;
- m.security = security;
- m.user = user;
- m.password = password ? password : "";
- m.from = smtp_from;
- m.from_name = org_name;
- m.to = to;
- m.subject = subject;
- m.body = body;
- m.attach_name = attach_name;
- m.attach = pdf;
- m.attach_len = pdf_len;
-
- char errbuf[512];
- if (smtp_send(&m, errbuf, sizeof errbuf) != 0) {
- fail(r, "SMTP_FAILED", errbuf[0] ? errbuf : "smtp send failed");
- goto done;
- }
-
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE invoices SET last_sent_at=?1,last_sent_to=?2"
- " WHERE org_id=?3 AND id=?4",
- -1, &st, NULL) != SQLITE_OK) {
- fail(r, "INTERNAL", "database error");
- goto done;
- }
- sqlite3_bind_text(st, 1, ts, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 2, to, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 3, r->org_id);
- sqlite3_bind_int64(st, 4, id);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- st = NULL;
- if (rc != SQLITE_DONE) {
- fail(r, "DB_BUSY", sqlite3_errmsg(r->db));
- goto done;
- }
-
- yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *a = yyjson_mut_obj(doc);
- yyjson_mut_doc_set_root(doc, a);
- yyjson_mut_obj_add_int(doc, a, "id", id);
- yyjson_mut_obj_add_strcpy(doc, a, "to", to);
- yyjson_mut_obj_add_strcpy(doc, a, "subject", subject);
- char *reqjson = yyjson_mut_write(doc, 0, NULL);
- yyjson_mut_doc_free(doc);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "invoice.send", reqjson ? 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, "sent_to", to);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "at", ts);
- res = o;
-
-done:
- free(smtp_host);
- free(smtp_port);
- free(smtp_user);
- free(smtp_from);
- free(smtp_security);
- free(smtp_password);
- free(password);
- free(body);
- free(pdf);
- return res;
-}
-
-/* ------------------------------------------------------------------ */
-/* 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;
-}
-
-/* ------------------------------------------------------------------ */
-/* voucher templates (konteringsmallar) */
-/* ------------------------------------------------------------------ */
-
-struct tpl_head {
- int64_t id;
- char name[128];
- char series[16];
- char description[256];
-};
-
-struct tpl_loaded {
- char account[16];
- char formula[256];
- char desc[128];
-};
-
-struct tpl_row_in {
- int64_t account_id;
- char account[16];
- char formula[256];
- char desc[128];
-};
-
-static 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;
-}
-
-static 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;
-}
-
-static 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;
-}
-
-static 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;
-}
-
-static int parse_template_rows(struct req *r, yyjson_val *rowsv,
- struct tpl_row_in **out, size_t *out_n)
-{
- *out = NULL;
- *out_n = 0;
- if (!rowsv || !yyjson_is_arr(rowsv))
- return fail(r, "INVALID_ARGS", "rows must be an array") ? -1 : -1;
- size_t n = yyjson_arr_size(rowsv);
- if (n == 0 || n > 100)
- return fail(r, "INVALID_ARGS",
- "a template needs between 1 and 100 rows")
- ? -1
- : -1;
- struct tpl_row_in *rows = xcalloc(n, sizeof *rows);
- size_t k = 0;
- yyjson_arr_iter it = yyjson_arr_iter_with(rowsv);
- yyjson_val *item;
- while ((item = yyjson_arr_iter_next(&it))) {
- if (!yyjson_is_obj(item)) {
- free(rows);
- fail(r, "INVALID_ARGS", "each row must be an object");
- return -1;
- }
- yyjson_val *av = yyjson_obj_get(item, "account");
- yyjson_val *fv = yyjson_obj_get(item, "formula");
- if (!av || !yyjson_is_str(av) || !fv || !yyjson_is_str(fv)) {
- free(rows);
- failf(r, "INVALID_ARGS",
- "row %zu: account and formula are required", k + 1);
- return -1;
- }
- const char *account = yyjson_get_str(av);
- const char *formula = yyjson_get_str(fv);
- if (!formula_valid(formula)) {
- free(rows);
- failf(r, "INVALID_ARGS", "row %zu: ogiltig formel '%s'", k + 1,
- formula);
- return -1;
- }
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db,
- "SELECT id FROM accounts WHERE org_id=?1"
- " AND number=?2",
- -1, &st, NULL) != SQLITE_OK) {
- free(rows);
- return fail(r, "INTERNAL", "database error") ? -1 : -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, account, -1, SQLITE_TRANSIENT);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- free(rows);
- failf(r, "ACCOUNT_NOT_FOUND", "row %zu: account %s does not exist",
- k + 1, account);
- return -1;
- }
- rows[k].account_id = sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
- snprintf(rows[k].account, sizeof rows[k].account, "%s", account);
- snprintf(rows[k].formula, sizeof rows[k].formula, "%s", formula);
- yyjson_val *dv = yyjson_obj_get(item, "description");
- if (dv && yyjson_is_str(dv))
- snprintf(rows[k].desc, sizeof rows[k].desc, "%s",
- yyjson_get_str(dv));
- k++;
- }
- *out = rows;
- *out_n = k;
- return 0;
-}
-
-static yyjson_mut_val *tpl_rows_json(struct req *r, sqlite3_stmt *st)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "line_no", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "account",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "formula",
- sq(sqlite3_column_text(st, 3)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
- sq(sqlite3_column_text(st, 4)));
- return o;
-}
-
-static yyjson_mut_val *h_template_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 t.id,t.name,t.series,t.description,t.active,"
- "(SELECT count(*) FROM voucher_template_rows tr"
- " WHERE tr.org_id=t.org_id AND tr.template_id=t.id)"
- " FROM voucher_templates t WHERE t.org_id=?1 AND t.active=1"
- " ORDER BY t.name"
- : "SELECT t.id,t.name,t.series,t.description,t.active,"
- "(SELECT count(*) FROM voucher_template_rows tr"
- " WHERE tr.org_id=t.org_id AND tr.template_id=t.id)"
- " FROM voucher_templates t WHERE t.org_id=?1 ORDER BY t.name";
- 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_val *o = yyjson_mut_arr_add_obj(r->rdoc, items);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "series",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
- sq(sqlite3_column_text(st, 3)));
- yyjson_mut_obj_add_bool(r->rdoc, o, "active",
- sqlite3_column_int(st, 4) != 0);
- yyjson_mut_obj_add_int(r->rdoc, o, "row_count",
- sqlite3_column_int64(st, 5));
- }
- sqlite3_finalize(st);
- yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
- return out;
-}
-
-static yyjson_mut_val *h_template_get(struct req *r)
-{
- int64_t id = 0;
- arg_int(r->args, "id", &id);
- const char *name = arg_str(r->args, "name");
- if (id <= 0 && !name)
- return fail(r, "INVALID_ARGS", "id or name is required");
- sqlite3_stmt *st = NULL;
- const char *sql =
- id > 0 ? "SELECT id,name,series,description,active"
- " FROM voucher_templates WHERE org_id=?1 AND id=?2"
- : "SELECT id,name,series,description,active"
- " FROM voucher_templates WHERE org_id=?1 AND name=?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 > 0)
- sqlite3_bind_int64(st, 2, id);
- else
- sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "template not found");
- }
- int64_t tpl = sqlite3_column_int64(st, 0);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", tpl);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "series",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
- sq(sqlite3_column_text(st, 3)));
- yyjson_mut_obj_add_bool(r->rdoc, o, "active",
- sqlite3_column_int(st, 4) != 0);
- sqlite3_finalize(st);
-
- yyjson_mut_val *rows = yyjson_mut_arr(r->rdoc);
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT tr.line_no,a.number,a.name,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) {
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, tpl);
- while (sqlite3_step(st) == SQLITE_ROW)
- yyjson_mut_arr_add_val(rows, tpl_rows_json(r, st));
- sqlite3_finalize(st);
- }
- yyjson_mut_obj_add_val(r->rdoc, o, "rows", rows);
- return o;
-}
-
-static yyjson_mut_val *h_template_create(struct req *r)
-{
- const char *name = arg_str(r->args, "name");
- const char *series = arg_str(r->args, "series");
- const char *description = arg_str(r->args, "description");
- if (!name || !*name)
- return fail(r, "INVALID_ARGS", "name is required");
- char *series_owned = NULL;
- if (!series || !*series) {
- series_owned = db_setting(r->db, r->org_id, "default_series");
- series = series_owned && *series_owned ? series_owned : "A";
- }
- if (!description)
- description = "";
- struct tpl_row_in *rows = NULL;
- size_t nrows = 0;
- if (parse_template_rows(
- r, r->args ? yyjson_obj_get(r->args, "rows") : NULL, &rows,
- &nrows) != 0)
- return NULL;
-
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "series", series);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", description);
- yyjson_mut_val *arr = yyjson_mut_arr(r->rdoc);
- for (size_t i = 0; i < nrows; i++) {
- yyjson_mut_val *ro = yyjson_mut_arr_add_obj(r->rdoc, arr);
- yyjson_mut_obj_add_strcpy(r->rdoc, ro, "account", rows[i].account);
- yyjson_mut_obj_add_strcpy(r->rdoc, ro, "formula", rows[i].formula);
- yyjson_mut_obj_add_strcpy(r->rdoc, ro, "description", rows[i].desc);
- }
- yyjson_mut_obj_add_val(r->rdoc, o, "rows", arr);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- free(rows);
- free(series_owned);
- return o;
- }
-
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- if (db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0) {
- free(rows);
- free(series_owned);
- return fail(r, "DB_BUSY", "could not start transaction");
- }
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db,
- "INSERT INTO voucher_templates(org_id,name,series,"
- "description,created_at) VALUES(?1,?2,?3,?4,?5)",
- -1, &st, NULL) != SQLITE_OK) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(rows);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 3, series, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 4, description, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 5, ts, -1, SQLITE_TRANSIENT);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(rows);
- if ((rc & 0xff) == SQLITE_CONSTRAINT)
- return fail(r, "CONFLICT", "a template with that name exists");
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- int64_t tpl = db_last_id(r->db);
- for (size_t i = 0; i < nrows; i++) {
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO voucher_template_rows(org_id,template_id,line_no,"
- "account_id,formula,description) VALUES(?1,?2,?3,?4,?5,?6)",
- -1, &st, NULL) != SQLITE_OK) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(rows);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, tpl);
- sqlite3_bind_int64(st, 3, (int64_t)i + 1);
- sqlite3_bind_int64(st, 4, rows[i].account_id);
- sqlite3_bind_text(st, 5, rows[i].formula, -1, SQLITE_TRANSIENT);
- if (rows[i].desc[0])
- sqlite3_bind_text(st, 6, rows[i].desc, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 6);
- rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(rows);
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- }
- free(rows);
- if (db_exec(r->db, "COMMIT", NULL) != 0)
- return fail(r, "INTERNAL", "commit failed");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "template.create", reqjson, "OK", NULL);
- free(reqjson);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", tpl);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "series", series);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", description);
- free(series_owned);
- return o;
-}
-
-static yyjson_mut_val *h_template_update(struct req *r)
-{
- int64_t id = 0;
- arg_int(r->args, "id", &id);
- const char *name_arg = arg_str(r->args, "name");
- if (id <= 0 && !name_arg)
- return fail(r, "INVALID_ARGS", "id or name is required");
- struct tpl_head head;
- char *err = NULL;
- if (load_template(r->db, r->org_id, id, name_arg, &head, &err) != 0) {
- yyjson_mut_val *res =
- fail(r, "NOT_FOUND", err ? err : "template not found");
- free(err);
- return res;
- }
- const char *new_name = arg_str(r->args, "name");
- const char *series = arg_str(r->args, "series");
- const char *description = arg_str(r->args, "description");
- int active = -1;
- arg_bool(r->args, "active", &active);
- yyjson_val *rowsv = r->args ? yyjson_obj_get(r->args, "rows") : NULL;
- struct tpl_row_in *rows = NULL;
- size_t nrows = 0;
- if (rowsv) {
- if (parse_template_rows(r, rowsv, &rows, &nrows) != 0)
- return NULL;
- }
- if (!new_name && !series && !description && active < 0 && !rowsv) {
- free(rows);
- return fail(r, "INVALID_ARGS", "nothing to update");
- }
-
- if (r->dry_run) {
- free(rows);
- return h_template_get(r);
- }
-
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- if (db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0) {
- free(rows);
- return fail(r, "DB_BUSY", "could not start transaction");
- }
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "UPDATE voucher_templates SET"
- " name=COALESCE(?2,name), series=COALESCE(?3,series),"
- " description=COALESCE(?4,description),"
- " 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) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(rows);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- if (new_name)
- sqlite3_bind_text(st, 2, new_name, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 2);
- if (series)
- sqlite3_bind_text(st, 3, series, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 3);
- if (description)
- sqlite3_bind_text(st, 4, description, -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, head.id);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(rows);
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- if (rowsv) {
- if (sqlite3_prepare_v2(
- r->db,
- "DELETE FROM voucher_template_rows WHERE org_id=?1"
- " AND template_id=?2",
- -1, &st, NULL) != SQLITE_OK) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(rows);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, head.id);
- rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(rows);
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- for (size_t i = 0; i < nrows; i++) {
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO voucher_template_rows(org_id,template_id,"
- "line_no,account_id,formula,description)"
- " VALUES(?1,?2,?3,?4,?5,?6)",
- -1, &st, NULL) != SQLITE_OK) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(rows);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, head.id);
- sqlite3_bind_int64(st, 3, (int64_t)i + 1);
- sqlite3_bind_int64(st, 4, rows[i].account_id);
- sqlite3_bind_text(st, 5, rows[i].formula, -1, SQLITE_TRANSIENT);
- if (rows[i].desc[0])
- sqlite3_bind_text(st, 6, rows[i].desc, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 6);
- rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(rows);
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- }
- }
- }
- free(rows);
- if (db_exec(r->db, "COMMIT", NULL) != 0)
- return fail(r, "INTERNAL", "commit failed");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "template.update", reqjson, "OK", NULL);
- free(reqjson);
- return h_template_get(r);
-}
-
-static yyjson_mut_val *h_template_archive(struct req *r)
-{
- int64_t id = 0;
- arg_int(r->args, "id", &id);
- const char *name = arg_str(r->args, "name");
- if (id <= 0 && !name)
- return fail(r, "INVALID_ARGS", "id or name is required");
- sqlite3_stmt *st = NULL;
- const char *sql =
- id > 0 ? "UPDATE voucher_templates SET active=0 WHERE org_id=?1"
- " AND id=?2"
- : "UPDATE voucher_templates SET active=0 WHERE org_id=?1"
- " AND name=?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 > 0)
- sqlite3_bind_int64(st, 2, id);
- else
- sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
- 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", "template not found");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "template.archive", reqjson, "OK", NULL);
- free(reqjson);
- return yyjson_mut_obj(r->rdoc);
-}
-
-static yyjson_mut_val *h_voucher_post(struct req *r)
-{
- const char *date = arg_str(r->args, "date");
- const char *description = arg_str(r->args, "description");
- const char *series = arg_str(r->args, "series");
- const char *client_ref = arg_str(r->args, "client_ref");
- int64_t corrects = 0;
- arg_int(r->args, "corrects_voucher", &corrects);
- if (!date)
- return fail(r, "INVALID_ARGS", "date is required");
-
- /* template application: {template, x} instead of {rows} */
- int64_t tpl_id = 0;
- const char *tpl_name = NULL;
- yyjson_val *tv = r->args ? yyjson_obj_get(r->args, "template") : NULL;
- if (tv) {
- if (yyjson_is_int(tv))
- tpl_id = yyjson_get_int(tv);
- else if (yyjson_is_str(tv))
- tpl_name = yyjson_get_str(tv);
- else
- return fail(r, "INVALID_ARGS", "template must be an id or a name");
- if (r->args && yyjson_obj_get(r->args, "rows"))
- return fail(r, "INVALID_ARGS",
- "use either rows or template, not both");
- }
- double x = 0;
- yyjson_val *xv = r->args ? yyjson_obj_get(r->args, "x") : NULL;
- if (xv) {
- if (yyjson_is_str(xv)) {
- int ok = 0;
- x = parse_kr_double(yyjson_get_str(xv), &ok);
- if (!ok)
- return fail(r, "INVALID_ARGS", "x must be a number in kronor");
- } else if (yyjson_is_real(xv)) {
- x = yyjson_get_real(xv);
- } else if (yyjson_is_int(xv)) {
- x = (double)yyjson_get_int(xv);
- } else {
- return fail(r, "INVALID_ARGS", "x must be a number in kronor");
- }
- }
-
- struct ledger_row *rows = NULL;
- size_t nrows = 0;
- char *owned_desc = NULL;
- char owned_series[16] = "";
- int rows_owned = 0;
-
- if (tv) {
- struct tpl_head head;
- char *err = NULL;
- if (load_template(r->db, r->org_id, tpl_id, tpl_name, &head, &err) !=
- 0) {
- yyjson_mut_val *res =
- fail(r, "NOT_FOUND", err ? err : "template not found");
- free(err);
- return res;
- }
- struct tpl_loaded *trows = NULL;
- size_t ntrows = 0;
- if (load_template_rows(r->db, r->org_id, head.id, &trows, &ntrows,
- &err) != 0) {
- yyjson_mut_val *res =
- fail(r, "INTERNAL", err ? err : "could not load template");
- free(err);
- return res;
- }
- struct template_row *in =
- xcalloc(ntrows ? ntrows : 1, sizeof *in);
- for (size_t i = 0; i < ntrows; i++) {
- in[i].account = trows[i].account;
- in[i].formula = trows[i].formula;
- in[i].description = trows[i].desc[0] ? trows[i].desc : NULL;
- }
- struct resolved_row *resolved =
- xcalloc(ntrows ? ntrows : 1, sizeof *resolved);
- char ferr[256] = "";
- if (formula_resolve_rows(in, ntrows, x, resolved, &nrows, ferr,
- sizeof ferr) != 0) {
- free(in);
- free(resolved);
- free(trows);
- return fail(r, "INVALID_ARGS",
- ferr[0] ? ferr : "template could not be resolved");
- }
- free(in);
- free(trows);
- rows = xcalloc(nrows, sizeof *rows);
- for (size_t i = 0; i < nrows; i++) {
- rows[i].account = xstrdup(resolved[i].account);
- rows[i].debit_ore = resolved[i].debit_ore;
- rows[i].credit_ore = resolved[i].credit_ore;
- rows[i].description =
- resolved[i].description[0] ? xstrdup(resolved[i].description)
- : NULL;
- }
- free(resolved);
- rows_owned = 1;
- if (!series && head.series[0])
- snprintf(owned_series, sizeof owned_series, "%s", head.series);
- if (!description && head.description[0])
- owned_desc = replace_x(head.description, x);
- if (!description && !owned_desc)
- owned_desc = xstrdup(head.name);
- } else {
- if (!description)
- return fail(r, "INVALID_ARGS",
- "date and description are required");
- if (parse_rows(r, r->args ? yyjson_obj_get(r->args, "rows") : NULL,
- &rows, &nrows) != 0)
- return NULL;
- }
-
- int64_t *att = NULL;
- size_t natt = 0;
- if (parse_attachment_ids(r, &att, &natt) != 0) {
- if (rows_owned)
- for (size_t i = 0; i < nrows; i++) {
- free((char *)rows[i].account);
- free((char *)rows[i].description);
- }
- free(rows);
- free(owned_desc);
- return NULL;
- }
-
- struct ledger_post_opts o;
- memset(&o, 0, sizeof o);
- o.org_id = r->org_id;
- o.user_id = r->sess->user_id;
- o.token_id = r->sess->token_id;
- o.date = date;
- o.description = description ? description : owned_desc;
- o.series = series ? series
- : (owned_series[0] ? owned_series : NULL);
- o.rows = rows;
- o.nrows = nrows;
- o.corrects_voucher_id = corrects;
- o.attachment_ids = att;
- o.n_attachments = natt;
- o.client_ref = client_ref;
- o.dry_run = r->dry_run;
-
- struct ledger_error e;
- char *json = NULL;
- int rc = ledger_post(r->db, &o, &e, &json);
- if (rows_owned)
- for (size_t i = 0; i < nrows; i++) {
- free((char *)rows[i].account);
- free((char *)rows[i].description);
- }
- free(rows);
- free(att);
- free(owned_desc);
- if (rc != 0) {
- if (e.has_details) {
- yyjson_mut_val *details = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, details, "difference_ore",
- e.difference_ore);
- r->err_details = details;
- }
- return fail(r, e.code ? e.code : "INTERNAL", e.msg);
- }
-
- if (!r->dry_run) {
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "voucher.post", reqjson, "OK", NULL);
- free(reqjson);
- }
- yyjson_mut_val *res = json_to_mut(r->rdoc, json);
- free(json);
- if (!res)
- return fail(r, "INTERNAL", "could not serialize result");
- return res;
-}
-
-static yyjson_mut_val *voucher_json(struct req *r, sqlite3_stmt *st)
-{
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_id",
- sqlite3_column_int64(st, 1));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "series",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_int(r->rdoc, o, "number", sqlite3_column_int64(st, 3));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "date",
- sq(sqlite3_column_text(st, 4)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
- sq(sqlite3_column_text(st, 5)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "source",
- sq(sqlite3_column_text(st, 6)));
- if (sqlite3_column_type(st, 7) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "corrects_voucher_id");
- else
- yyjson_mut_obj_add_int(r->rdoc, o, "corrects_voucher_id",
- sqlite3_column_int64(st, 7));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at",
- sq(sqlite3_column_text(st, 8)));
- yyjson_mut_obj_add_int(r->rdoc, o, "created_by_user",
- sqlite3_column_int64(st, 9));
- char hex[65];
- const void *hb = sqlite3_column_blob(st, 10);
- if (hb && sqlite3_column_bytes(st, 10) == 32) {
- util_hex((const unsigned char *)hb, 32, hex);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "hash_prev", hex);
- }
- const void *vb = sqlite3_column_blob(st, 11);
- if (vb && sqlite3_column_bytes(st, 11) == 32) {
- util_hex((const unsigned char *)vb, 32, hex);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "hash", hex);
- }
- return o;
-}
-
-#define VOUCHER_COLUMNS \
- "id,fiscal_year_id,series,number,date,description,source," \
- "corrects_voucher_id,created_at,created_by_user,hash_prev,hash"
-
-static yyjson_mut_val *h_voucher_get(struct req *r)
-{
- int64_t id = 0;
- if (!arg_int(r->args, "id", &id) || id <= 0)
- return fail(r, "INVALID_ARGS", "id is required");
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT " VOUCHER_COLUMNS " FROM vouchers 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);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "voucher not found");
- }
- yyjson_mut_val *o = voucher_json(r, st);
- sqlite3_finalize(st);
-
- yyjson_mut_val *rows = yyjson_mut_arr(r->rdoc);
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT a.number,a.name,r.debit_ore,r.credit_ore,"
- "COALESCE(r.description,'') FROM voucher_rows r"
- " JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
- " WHERE r.org_id=?1 AND r.voucher_id=?2 ORDER BY r.line_no",
- -1, &st, NULL) == SQLITE_OK) {
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, id);
- while (sqlite3_step(st) == SQLITE_ROW) {
- yyjson_mut_val *ro = yyjson_mut_arr_add_obj(r->rdoc, rows);
- yyjson_mut_obj_add_strcpy(r->rdoc, ro, "account",
- sq(sqlite3_column_text(st, 0)));
- yyjson_mut_obj_add_strcpy(r->rdoc, ro, "name",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_int(r->rdoc, ro, "debit_ore",
- sqlite3_column_int64(st, 2));
- yyjson_mut_obj_add_int(r->rdoc, ro, "credit_ore",
- sqlite3_column_int64(st, 3));
- yyjson_mut_obj_add_strcpy(r->rdoc, ro, "description",
- sq(sqlite3_column_text(st, 4)));
- }
- sqlite3_finalize(st);
- }
- yyjson_mut_obj_add_val(r->rdoc, o, "rows", rows);
-
- yyjson_mut_val *atts = yyjson_mut_arr(r->rdoc);
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT a.id,a.filename FROM voucher_attachments va"
- " JOIN attachments a ON a.org_id=va.org_id AND a.id=va.attachment_id"
- " WHERE va.org_id=?1 AND va.voucher_id=?2 ORDER BY a.id",
- -1, &st, NULL) == SQLITE_OK) {
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, id);
- while (sqlite3_step(st) == SQLITE_ROW) {
- yyjson_mut_val *ao = yyjson_mut_arr_add_obj(r->rdoc, atts);
- yyjson_mut_obj_add_int(r->rdoc, ao, "id",
- sqlite3_column_int64(st, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, ao, "filename",
- sq(sqlite3_column_text(st, 1)));
- }
- sqlite3_finalize(st);
- }
- yyjson_mut_obj_add_val(r->rdoc, o, "attachments", atts);
- return o;
-}
-
-static yyjson_mut_val *h_voucher_list(struct req *r)
-{
- int64_t fy = 0, cursor = 0, limit = 100, account_id = 0;
- arg_int(r->args, "fiscal_year", &fy);
- arg_int(r->args, "cursor", &cursor);
- arg_int(r->args, "limit", &limit);
- if (limit < 1)
- limit = 100;
- if (limit > 1000)
- limit = 1000;
- const char *series = arg_str(r->args, "series");
- const char *from = arg_str(r->args, "from");
- const char *to = arg_str(r->args, "to");
- const char *text = arg_str(r->args, "text");
- const char *account = arg_str(r->args, "account");
- if (account) {
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT id FROM accounts WHERE org_id=?1 AND number=?2", -1,
- &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, account, -1, SQLITE_TRANSIENT);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "account not found");
- }
- account_id = sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
- }
-
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT v.id,v.series,v.number,v.date,v.description,v.source,"
- "v.corrects_voucher_id,"
- "(SELECT count(*) FROM voucher_rows r WHERE r.org_id=v.org_id"
- " AND r.voucher_id=v.id),"
- "(SELECT count(*) FROM voucher_attachments va WHERE va.org_id=v.org_id"
- " AND va.voucher_id=v.id)"
- " FROM vouchers v WHERE v.org_id=?1"
- " AND (?2=0 OR v.fiscal_year_id=?2)"
- " AND (?3='' OR v.series=?3)"
- " AND (?4='' OR v.date>=?4)"
- " AND (?5='' OR v.date<=?5)"
- " AND (?6=0 OR EXISTS(SELECT 1 FROM voucher_rows r2"
- " WHERE r2.org_id=v.org_id AND r2.voucher_id=v.id"
- " AND r2.account_id=?6))"
- " AND (?7='' OR v.description LIKE '%'||?7||'%')"
- " AND v.id>?8 ORDER BY v.id LIMIT ?9",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, fy);
- sqlite3_bind_text(st, 3, series ? series : "", -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 4, from ? from : "", -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 5, to ? to : "", -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 6, account_id);
- sqlite3_bind_text(st, 7, text ? text : "", -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 8, cursor);
- sqlite3_bind_int64(st, 9, limit);
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- int64_t last = cursor, n = 0;
- while (sqlite3_step(st) == SQLITE_ROW) {
- n++;
- last = sqlite3_column_int64(st, 0);
- yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", last);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "series",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_int(r->rdoc, o, "number",
- sqlite3_column_int64(st, 2));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "date",
- sq(sqlite3_column_text(st, 3)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
- sq(sqlite3_column_text(st, 4)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "source",
- sq(sqlite3_column_text(st, 5)));
- if (sqlite3_column_type(st, 6) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "corrects_voucher_id");
- else
- yyjson_mut_obj_add_int(r->rdoc, o, "corrects_voucher_id",
- sqlite3_column_int64(st, 6));
- yyjson_mut_obj_add_int(r->rdoc, o, "row_count",
- sqlite3_column_int64(st, 7));
- yyjson_mut_obj_add_int(r->rdoc, o, "attachment_count",
- sqlite3_column_int64(st, 8));
- }
- sqlite3_finalize(st);
- yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
- if (n == limit)
- yyjson_mut_obj_add_int(r->rdoc, out, "next_cursor", last);
- else
- yyjson_mut_obj_add_null(r->rdoc, out, "next_cursor");
- return out;
-}
-
-static yyjson_mut_val *h_voucher_correct(struct req *r)
-{
- int64_t id = 0;
- const char *description = arg_str(r->args, "description");
- const char *date = arg_str(r->args, "date");
- const char *client_ref = arg_str(r->args, "client_ref");
- if (!arg_int(r->args, "voucher", &id) || id <= 0)
- return fail(r, "INVALID_ARGS", "voucher is required");
- if (!description || !*description)
- return fail(r, "INVALID_ARGS", "description is required");
-
- sqlite3_stmt *st = NULL;
- char series[16] = "", orig_date[16] = "";
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT series,date FROM vouchers 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);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "voucher not found");
- }
- snprintf(series, sizeof series, "%s", sqlite3_column_text(st, 0));
- snprintf(orig_date, sizeof orig_date, "%s", sqlite3_column_text(st, 1));
- sqlite3_finalize(st);
-
- struct ledger_row *rows = NULL;
- size_t nrows = 0, cap = 0;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT a.number,r.debit_ore,r.credit_ore,"
- "COALESCE(r.description,'') FROM voucher_rows r"
- " JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
- " WHERE r.org_id=?1 AND r.voucher_id=?2 ORDER BY r.line_no",
- -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);
- while (sqlite3_step(st) == SQLITE_ROW) {
- if (nrows == cap) {
- cap = cap ? cap * 2 : 8;
- rows = xrealloc(rows, cap * sizeof *rows);
- }
- memset(&rows[nrows], 0, sizeof rows[nrows]);
- rows[nrows].account = xstrdup(sq(sqlite3_column_text(st, 0)));
- rows[nrows].debit_ore = sqlite3_column_int64(st, 2);
- rows[nrows].credit_ore = sqlite3_column_int64(st, 1);
- const unsigned char *d = sqlite3_column_text(st, 3);
- rows[nrows].description = d && *d ? xstrdup((const char *)d) : NULL;
- nrows++;
- }
- sqlite3_finalize(st);
- if (nrows == 0)
- return fail(r, "INTERNAL", "voucher has no rows");
-
- struct ledger_post_opts o;
- memset(&o, 0, sizeof o);
- o.org_id = r->org_id;
- o.user_id = r->sess->user_id;
- o.token_id = r->sess->token_id;
- o.date = date ? date : orig_date;
- o.description = description;
- o.series = series;
- o.rows = rows;
- o.nrows = nrows;
- o.corrects_voucher_id = id;
- o.client_ref = client_ref;
- o.dry_run = r->dry_run;
-
- struct ledger_error e;
- char *json = NULL;
- int rc = ledger_post(r->db, &o, &e, &json);
- for (size_t i = 0; i < nrows; i++) {
- free((char *)rows[i].account);
- free((char *)rows[i].description);
- }
- free(rows);
- if (rc != 0)
- return fail(r, e.code ? e.code : "INTERNAL", e.msg);
- if (!r->dry_run) {
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "voucher.correct", reqjson, "OK", NULL);
- free(reqjson);
- }
- yyjson_mut_val *res = json_to_mut(r->rdoc, json);
- free(json);
- if (!res)
- return fail(r, "INTERNAL", "could not serialize result");
- return res;
-}
-
-/* ------------------------------------------------------------------ */
-/* attachments */
-/* ------------------------------------------------------------------ */
-
-static int link_attachment(struct req *r, int64_t aid, int64_t vid,
- char **errmsg, const char **errcode)
-{
- *errcode = "INTERNAL";
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT (SELECT count(*) FROM vouchers WHERE org_id=?1 AND id=?2),"
- "(SELECT count(*) FROM voucher_attachments WHERE org_id=?1"
- " AND voucher_id=?2 AND attachment_id=?3),"
- "(SELECT count(*) FROM attachments WHERE org_id=?1 AND id=?3)",
- -1, &st, NULL) != SQLITE_OK) {
- *errmsg = xstrdup("database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, vid);
- sqlite3_bind_int64(st, 3, aid);
- int64_t vexists = 0, linked = 0, aexists = 0;
- if (sqlite3_step(st) == SQLITE_ROW) {
- vexists = sqlite3_column_int64(st, 0);
- linked = sqlite3_column_int64(st, 1);
- aexists = sqlite3_column_int64(st, 2);
- }
- sqlite3_finalize(st);
- if (!aexists) {
- *errmsg = xstrdup("attachment not found");
- *errcode = "NOT_FOUND";
- return -1;
- }
- if (!vexists) {
- *errmsg = xstrdup("voucher not found");
- *errcode = "NOT_FOUND";
- return -1;
- }
- if (linked) {
- *errmsg = xstrdup("attachment is already linked");
- *errcode = "CONFLICT";
- return -1;
- }
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO voucher_attachments(org_id,voucher_id,attachment_id,"
- "created_at) VALUES(?1,?2,?3,?4)",
- -1, &st, NULL) != SQLITE_OK) {
- *errmsg = xstrdup("database error");
- return -1;
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, vid);
- sqlite3_bind_int64(st, 3, aid);
- sqlite3_bind_text(st, 4, ts, -1, SQLITE_TRANSIENT);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE) {
- *errmsg = xstrdup("could not link attachment");
- return -1;
- }
- return 0;
-}
-
-static yyjson_mut_val *h_attachment_put(struct req *r)
-{
- const char *filename = arg_str(r->args, "filename");
- const char *mime = arg_str(r->args, "mime");
- const char *b64 = arg_str(r->args, "content_base64");
- int64_t voucher_id = 0;
- arg_int(r->args, "voucher_id", &voucher_id);
- if (!filename || !*filename || !b64)
- return fail(r, "INVALID_ARGS",
- "filename and content_base64 are required");
- if (!mime || !*mime)
- mime = "application/octet-stream";
- unsigned char *content = NULL;
- size_t len = 0;
- if (util_b64_decode(b64, strlen(b64), &content, &len) != 0)
- return fail(r, "INVALID_ARGS", "content_base64 is not valid base64");
- if ((long)len > g_cfg.max_attachment_bytes) {
- free(content);
- return failf(r, "TOO_LARGE", "attachment exceeds %ld bytes",
- g_cfg.max_attachment_bytes);
- }
- unsigned char hash[32];
- util_sha256(content, len, hash);
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
-
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT id FROM attachments WHERE org_id=?1 AND sha256=?2"
- " AND filename=?3",
- -1, &st, NULL) != SQLITE_OK) {
- free(content);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_blob(st, 2, hash, 32, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 3, filename, -1, SQLITE_TRANSIENT);
- int64_t existing = 0;
- if (sqlite3_step(st) == SQLITE_ROW)
- existing = sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
-
- if (existing) {
- free(content);
- if (voucher_id) {
- char *emsg = NULL;
- const char *ecode = NULL;
- if (link_attachment(r, existing, voucher_id, &emsg, &ecode) != 0) {
- yyjson_mut_val *res =
- fail(r, ecode ? ecode : "INTERNAL", emsg);
- free(emsg);
- return res;
- }
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "attachment.link", reqjson, "OK", NULL);
- free(reqjson);
- }
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", existing);
- yyjson_mut_obj_add_bool(r->rdoc, o, "replayed", true);
- return o;
- }
-
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO attachments(org_id,sha256,filename,mime,size_bytes,"
- "content,created_at,created_by) VALUES(?1,?2,?3,?4,?5,?6,?7,?8)",
- -1, &st, NULL) != SQLITE_OK) {
- free(content);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_blob(st, 2, hash, 32, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 3, filename, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 4, mime, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 5, (int64_t)len);
- sqlite3_bind_blob(st, 6, content, (int)len, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 7, ts, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 8, r->sess->user_id);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- free(content);
- if (rc != SQLITE_DONE)
- return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
- int64_t id = db_last_id(r->db);
-
- if (voucher_id) {
- char *emsg = NULL;
- const char *ecode = NULL;
- if (link_attachment(r, id, voucher_id, &emsg, &ecode) != 0) {
- yyjson_mut_val *res = fail(r, ecode ? ecode : "INTERNAL", emsg);
- free(emsg);
- return res;
- }
- }
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "attachment.put", 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_int(r->rdoc, o, "size_bytes", (int64_t)len);
- return o;
-}
-
-static yyjson_mut_val *h_attachment_link(struct req *r)
-{
- int64_t id = 0, voucher_id = 0;
- arg_int(r->args, "id", &id);
- arg_int(r->args, "voucher_id", &voucher_id);
- if (id <= 0 || voucher_id <= 0)
- return fail(r, "INVALID_ARGS", "id and voucher_id are required");
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
- }
- char *emsg = NULL;
- const char *ecode = NULL;
- if (link_attachment(r, id, voucher_id, &emsg, &ecode) != 0) {
- yyjson_mut_val *e = fail(r, ecode ? ecode : "INTERNAL", emsg);
- free(emsg);
- return e;
- }
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "attachment.link", 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_int(r->rdoc, o, "voucher_id", voucher_id);
- return o;
-}
-
-static yyjson_mut_val *h_attachment_unlink(struct req *r)
-{
- int64_t id = 0, voucher_id = 0;
- arg_int(r->args, "id", &id);
- arg_int(r->args, "voucher_id", &voucher_id);
- if (id <= 0 || voucher_id <= 0)
- return fail(r, "INVALID_ARGS", "id and voucher_id are required");
- if (r->dry_run) {
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- 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 voucher_attachments WHERE org_id=?1"
- " AND voucher_id=?2 AND attachment_id=?3",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, voucher_id);
- sqlite3_bind_int64(st, 3, 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", "link not found");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "attachment.unlink", reqjson, "OK", NULL);
- free(reqjson);
- return yyjson_mut_obj(r->rdoc);
-}
-
-static yyjson_mut_val *h_attachment_get(struct req *r)
-{
- int64_t id = 0;
- if (!arg_int(r->args, "id", &id) || id <= 0)
- return fail(r, "INVALID_ARGS", "id is required");
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT filename,mime,size_bytes,content,sha256,created_at"
- " FROM attachments 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);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "attachment not found");
- }
- const void *content = sqlite3_column_blob(st, 3);
- size_t len = (size_t)sqlite3_column_bytes(st, 3);
- char *b64 = util_b64(content ? content : (const unsigned char *)"", len);
- char hex[65];
- const void *hb = sqlite3_column_blob(st, 4);
- if (hb && sqlite3_column_bytes(st, 4) == 32)
- util_hex((const unsigned char *)hb, 32, hex);
- else
- hex[0] = '\0';
- 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, "filename",
- sq(sqlite3_column_text(st, 0)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "mime",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_int(r->rdoc, o, "size_bytes",
- sqlite3_column_int64(st, 2));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "sha256", hex);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at",
- sq(sqlite3_column_text(st, 5)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "content_base64", b64);
- free(b64);
- sqlite3_finalize(st);
- return o;
-}
-
-static yyjson_mut_val *h_attachment_list(struct req *r)
-{
- int64_t voucher_id = 0, cursor = 0, limit = 100;
- int unlinked = 0;
- arg_int(r->args, "voucher_id", &voucher_id);
- arg_int(r->args, "cursor", &cursor);
- arg_int(r->args, "limit", &limit);
- arg_bool(r->args, "unlinked", &unlinked);
- if (limit < 1)
- limit = 100;
- if (limit > 1000)
- limit = 1000;
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT a.id,a.filename,a.mime,a.size_bytes,a.created_at,a.sha256,"
- "(SELECT va.voucher_id FROM voucher_attachments va"
- " WHERE va.org_id=a.org_id AND va.attachment_id=a.id"
- " AND (?2=0 OR va.voucher_id=?2)"
- " ORDER BY va.voucher_id LIMIT 1)"
- " FROM attachments a WHERE a.org_id=?1"
- " AND (?2=0 OR EXISTS(SELECT 1 FROM voucher_attachments va2"
- " WHERE va2.org_id=a.org_id AND va2.attachment_id=a.id"
- " AND va2.voucher_id=?2))"
- " AND (?3=0 OR NOT EXISTS(SELECT 1 FROM voucher_attachments va3"
- " WHERE va3.org_id=a.org_id AND va3.attachment_id=a.id))"
- " AND a.id>?4 ORDER BY a.id LIMIT ?5",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, voucher_id);
- sqlite3_bind_int(st, 3, unlinked);
- sqlite3_bind_int64(st, 4, cursor);
- sqlite3_bind_int64(st, 5, limit);
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- int64_t n = 0, last = cursor;
- while (sqlite3_step(st) == SQLITE_ROW) {
- n++;
- last = sqlite3_column_int64(st, 0);
- yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items);
- yyjson_mut_obj_add_int(r->rdoc, o, "id", last);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "filename",
- sq(sqlite3_column_text(st, 1)));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "mime",
- sq(sqlite3_column_text(st, 2)));
- yyjson_mut_obj_add_int(r->rdoc, o, "size_bytes",
- sqlite3_column_int64(st, 3));
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at",
- sq(sqlite3_column_text(st, 4)));
- char hex[65];
- const void *hb = sqlite3_column_blob(st, 5);
- if (hb && sqlite3_column_bytes(st, 5) == 32)
- util_hex((const unsigned char *)hb, 32, hex);
- else
- hex[0] = '\0';
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "sha256", hex);
- if (sqlite3_column_type(st, 6) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, o, "voucher_id");
- else
- yyjson_mut_obj_add_int(r->rdoc, o, "voucher_id",
- sqlite3_column_int64(st, 6));
- }
- sqlite3_finalize(st);
- yyjson_mut_val *out = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, out, "items", items);
- if (n == limit)
- yyjson_mut_obj_add_int(r->rdoc, out, "next_cursor", last);
- else
- yyjson_mut_obj_add_null(r->rdoc, out, "next_cursor");
- return out;
-}
-
-/* ------------------------------------------------------------------ */
-/* reports and SIE */
-/* ------------------------------------------------------------------ */
-
-static 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;
-}
-
-static 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;
-}
-
-static yyjson_mut_val *h_report_trial_balance(struct req *r)
-{
- int64_t fy = 0;
- if (req_fy(r, &fy) != 0)
- return NULL;
- int include_zero = 0;
- arg_bool(r->args, "include_zero", &include_zero);
- char *err = NULL;
- yyjson_mut_val *res = report_trial_balance(
- r->rdoc, r->db, r->org_id, fy, arg_str(r->args, "from"),
- arg_str(r->args, "to"), include_zero, &err);
- if (!res) {
- yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "report failed");
- free(err);
- return e;
- }
- return res;
-}
-
-static yyjson_mut_val *h_report_income_statement(struct req *r)
-{
- int64_t fy = 0;
- if (req_fy(r, &fy) != 0)
- return NULL;
- char *err = NULL;
- yyjson_mut_val *res = report_income_statement(
- r->rdoc, r->db, r->org_id, fy, arg_str(r->args, "from"),
- arg_str(r->args, "to"), &err);
- if (!res) {
- yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "report failed");
- free(err);
- return e;
- }
- return res;
-}
-
-static yyjson_mut_val *h_report_balance_sheet(struct req *r)
-{
- int64_t fy = 0;
- if (req_fy(r, &fy) != 0)
- return NULL;
- char *err = NULL;
- yyjson_mut_val *res = report_balance_sheet(
- r->rdoc, r->db, r->org_id, fy, arg_str(r->args, "to"), &err);
- if (!res) {
- yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "report failed");
- free(err);
- return e;
- }
- return res;
-}
-
-static yyjson_mut_val *h_report_vat(struct req *r)
-{
- const char *from = arg_str(r->args, "from");
- const char *to = arg_str(r->args, "to");
- if (!from || !to)
- return fail(r, "INVALID_ARGS", "from and to are required");
- char *err = NULL;
- yyjson_mut_val *res =
- report_vat(r->rdoc, r->db, r->org_id, from, to, &err);
- if (!res) {
- yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "report failed");
- free(err);
- return e;
- }
- return res;
-}
-
-static yyjson_mut_val *h_sru_export(struct req *r)
-{
- int64_t fy = 0;
- if (req_fy(r, &fy) != 0)
- return NULL;
- char *err = NULL;
- yyjson_mut_val *res =
- sru_export(r->rdoc, r->db, r->org_id, fy, r->args, &err);
- if (!res) {
- yyjson_mut_val *e =
- fail(r, "INVALID_ARGS", err ? err : "could not build the SRU files");
- free(err);
- return e;
- }
- return res;
-}
-
-static yyjson_mut_val *h_report_general_ledger(struct req *r)
-{
- int64_t fy = 0;
- if (req_fy(r, &fy) != 0)
- return NULL;
- yyjson_val *accounts = yyjson_obj_get(r->args, "accounts");
- if (accounts && !yyjson_is_arr(accounts))
- return fail(r, "INVALID_ARGS", "accounts must be an array of strings");
- char *err = NULL;
- yyjson_mut_val *res = report_general_ledger(
- r->rdoc, r->db, r->org_id, fy, arg_str(r->args, "from"),
- arg_str(r->args, "to"), accounts, &err);
- if (!res) {
- yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "report failed");
- free(err);
- return e;
- }
- return res;
-}
-
-static yyjson_mut_val *h_report_voucher_list(struct req *r)
-{
- int64_t fy = 0;
- if (req_fy(r, &fy) != 0)
- return NULL;
- char *err = NULL;
- yyjson_mut_val *res = report_voucher_list(
- r->rdoc, r->db, r->org_id, fy, arg_str(r->args, "series"), &err);
- if (!res) {
- yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "report failed");
- free(err);
- return e;
- }
- return res;
-}
-
-static yyjson_mut_val *h_report_vat_eskd(struct req *r)
-{
- const char *from = arg_str(r->args, "from");
- const char *to = arg_str(r->args, "to");
- if (!from || !to)
- return fail(r, "INVALID_ARGS", "from and to are required");
- char *err = NULL;
- yyjson_mut_val *res =
- report_vat_eskd(r->rdoc, r->db, r->org_id, from, to,
- arg_str(r->args, "upplysning"), &err);
- if (!res) {
- yyjson_mut_val *e = fail(r, "INVALID_ARGS",
- err ? err : "could not build the eSKD file");
- free(err);
- return e;
- }
- return res;
-}
-
-/* True for P&L accounts that take part in the taxable result (3xxx-88xx). */
-static int bokslut_pl_account(const char *number)
-{
- int n = atoi(number);
- return n >= 3000 && n <= 8899;
-}
-
-static yyjson_mut_val *h_bokslut_post(struct req *r)
-{
- int64_t fy_id = 0;
- if (req_fy(r, &fy_id) != 0)
- return NULL;
- char fy_label[64] = "", fy_start[16] = "", fy_end[16] = "";
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(r->db,
- "SELECT label,start_date,end_date FROM fiscal_years"
- " 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, fy_id);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "fiscal year not found");
- }
- snprintf(fy_label, sizeof fy_label, "%s",
- (const char *)sqlite3_column_text(st, 0));
- snprintf(fy_start, sizeof fy_start, "%s",
- (const char *)sqlite3_column_text(st, 1));
- snprintf(fy_end, sizeof fy_end, "%s",
- (const char *)sqlite3_column_text(st, 2));
- sqlite3_finalize(st);
- const char *date = arg_str(r->args, "date");
- if (!date)
- date = fy_end;
- else if (!util_parse_iso_date(date))
- return fail(r, "INVALID_ARGS", "date must be YYYY-MM-DD");
-
- yyjson_val *earr = yyjson_obj_get(r->args, "entries");
- if (earr && !yyjson_is_arr(earr))
- return fail(r, "INVALID_ARGS", "entries must be an array");
- size_t nent = earr ? yyjson_arr_size(earr) : 0;
- if (nent > 31)
- return fail(r, "INVALID_ARGS", "too many entries");
- struct ledger_row erows[64];
- size_t nerows = 0;
- memset(erows, 0, sizeof erows);
- for (size_t i = 0; i < nent; i++) {
- yyjson_val *e = yyjson_arr_get(earr, i);
- const char *da =
- yyjson_get_str(yyjson_obj_get(e, "debit_account"));
- const char *ca =
- yyjson_get_str(yyjson_obj_get(e, "credit_account"));
- const char *ed =
- yyjson_get_str(yyjson_obj_get(e, "description"));
- int64_t amt = 0;
- yyjson_val *av = yyjson_obj_get(e, "amount_ore");
- if (av && yyjson_is_int(av))
- amt = yyjson_get_sint(av);
- if (!da || !*da || !ca || !*ca || amt <= 0)
- return fail(r, "INVALID_ARGS",
- "each entry needs debit_account, credit_account and"
- " amount_ore > 0");
- erows[nerows++] = (struct ledger_row){ da, amt, 0, ed };
- erows[nerows++] = (struct ledger_row){ ca, 0, amt, ed };
- }
- int64_t fond = 0;
- arg_int(r->args, "periodiseringsfond_ore", &fond);
- if (fond < 0)
- return fail(r, "INVALID_ARGS", "periodiseringsfond_ore must be >= 0");
- if (fond > 0) {
- if (nerows + 2 > 64)
- return fail(r, "INVALID_ARGS", "too many entries");
- erows[nerows++] = (struct ledger_row){ "8811", fond, 0, NULL };
- erows[nerows++] = (struct ledger_row){ "2110", 0, fond, NULL };
- }
-
- int64_t result_before = 0;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT COALESCE(SUM(r.credit_ore)-SUM(r.debit_ore),0)"
- " FROM voucher_rows r"
- " JOIN vouchers v ON v.org_id=r.org_id AND v.id=r.voucher_id"
- " JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
- " WHERE r.org_id=?1 AND v.fiscal_year_id=?2 AND v.series<>'IB'"
- " AND a.type IN ('revenue','expense') AND a.number NOT LIKE '89%'",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, fy_id);
- if (sqlite3_step(st) == SQLITE_ROW)
- result_before = sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
-
- int64_t delta = 0;
- for (size_t i = 0; i < nerows; i++)
- if (bokslut_pl_account(erows[i].account))
- delta += erows[i].credit_ore - erows[i].debit_ore;
-
- double tax_rate = 20.6;
- yyjson_val *trv = yyjson_obj_get(r->args, "tax_rate");
- if (trv) {
- if (yyjson_is_real(trv))
- tax_rate = yyjson_get_real(trv);
- else if (yyjson_is_int(trv))
- tax_rate = (double)yyjson_get_sint(trv);
- else
- return fail(r, "INVALID_ARGS", "tax_rate must be a number");
- if (tax_rate < 0 || tax_rate > 100)
- return fail(r, "INVALID_ARGS", "tax_rate must be 0-100");
- }
- long bp = (long)(tax_rate * 100.0 + 0.5);
- int64_t taxable = result_before + delta;
- int64_t tax = taxable > 0 ? taxable * bp / 10000 : 0;
- int64_t after = taxable - tax;
- int dispose = 1;
- arg_bool(r->args, "dispose", &dispose);
-
- struct {
- const char *desc;
- const struct ledger_row *rows;
- size_t nrows;
- } plan[3];
- size_t np = 0;
- if (nerows > 0)
- plan[np++] = (typeof(plan[0])){ "Bokslutsdispositioner", erows,
- nerows };
- struct ledger_row taxrows[2] = {
- { "8910", tax, 0, NULL }, { "2512", 0, tax, NULL }
- };
- if (tax != 0)
- plan[np++] = (typeof(plan[0])){ "Skatt på årets resultat", taxrows, 2 };
- struct ledger_row drows[2];
- if (dispose && after != 0) {
- if (after > 0) {
- drows[0] = (struct ledger_row){ "8999", after, 0, NULL };
- drows[1] = (struct ledger_row){ "2099", 0, after, NULL };
- } else {
- drows[0] = (struct ledger_row){ "2099", -after, 0, NULL };
- drows[1] = (struct ledger_row){ "8999", 0, -after, NULL };
- }
- plan[np++] = (typeof(plan[0])){ "Resultatdisposition", drows, 2 };
- }
-
- yyjson_mut_val *vouchers = yyjson_mut_arr(r->rdoc);
- for (size_t i = 0; i < np; i++) {
- struct ledger_post_opts o;
- memset(&o, 0, sizeof o);
- o.org_id = r->org_id;
- o.user_id = r->sess->user_id;
- o.token_id = r->sess->token_id;
- o.date = date;
- o.description = plan[i].desc;
- o.rows = plan[i].rows;
- o.nrows = plan[i].nrows;
- o.dry_run = r->dry_run;
- struct ledger_error e;
- char *json = NULL;
- if (ledger_post(r->db, &o, &e, &json) != 0) {
- yyjson_mut_val *res =
- fail(r, e.code ? e.code : "INTERNAL", e.msg);
- free(json);
- return res;
- }
- yyjson_mut_val *vo = yyjson_mut_arr_add_obj(r->rdoc, vouchers);
- yyjson_mut_obj_add_strcpy(r->rdoc, vo, "description", plan[i].desc);
- yyjson_mut_val *rows = yyjson_mut_arr(r->rdoc);
- for (size_t k = 0; k < plan[i].nrows; k++) {
- yyjson_mut_val *ro = yyjson_mut_arr_add_obj(r->rdoc, rows);
- yyjson_mut_obj_add_strcpy(r->rdoc, ro, "account",
- plan[i].rows[k].account);
- yyjson_mut_obj_add_int(r->rdoc, ro, "debit_ore",
- plan[i].rows[k].debit_ore);
- yyjson_mut_obj_add_int(r->rdoc, ro, "credit_ore",
- plan[i].rows[k].credit_ore);
- }
- yyjson_mut_obj_add_val(r->rdoc, vo, "rows", rows);
- yyjson_mut_val *pv = json_to_mut(r->rdoc, json);
- free(json);
- if (pv)
- yyjson_mut_obj_add_val(r->rdoc, vo, "voucher", pv);
- }
- if (!r->dry_run) {
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "bokslut.post", reqjson, "OK", NULL);
- free(reqjson);
- }
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_val *fy = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, fy, "id", fy_id);
- yyjson_mut_obj_add_strcpy(r->rdoc, fy, "label", fy_label);
- yyjson_mut_obj_add_strcpy(r->rdoc, fy, "start_date", fy_start);
- yyjson_mut_obj_add_strcpy(r->rdoc, fy, "end_date", fy_end);
- yyjson_mut_obj_add_val(r->rdoc, o, "fiscal_year", fy);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "date", date);
- yyjson_mut_obj_add_int(r->rdoc, o, "result_before_ore", result_before);
- yyjson_mut_obj_add_int(r->rdoc, o, "entries_ore", delta);
- yyjson_mut_obj_add_int(r->rdoc, o, "taxable_ore", taxable);
- yyjson_mut_obj_add_int(r->rdoc, o, "tax_ore", tax);
- yyjson_mut_obj_add_int(r->rdoc, o, "result_after_ore", after);
- yyjson_mut_obj_add_val(r->rdoc, o, "vouchers", vouchers);
- return o;
-}
-
-
-static 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;
-}
-
-static yyjson_mut_val *h_sie_export(struct req *r)
-{
- int64_t fy = 0;
- if (req_fy(r, &fy) != 0)
- return NULL;
- int inline_out = 0;
- arg_bool(r->args, "inline", &inline_out);
-
- if (mkdir_p(g_cfg.export_dir, 0700) != 0)
- return fail(r, "INTERNAL", "cannot create export directory");
- char label[64] = "";
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT label FROM fiscal_years 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, fy);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "fiscal year not found");
- }
- snprintf(label, sizeof label, "%s", sqlite3_column_text(st, 0));
- sqlite3_finalize(st);
- for (char *p = label; *p; p++)
- if (*p == '/')
- *p = '-';
- char path[4096];
- snprintf(path, sizeof path, "%s/%lld_%s.se", g_cfg.export_dir,
- (long long)r->org_id, label);
- char *err = NULL;
- if (sie_export_file(r->db, r->org_id, fy, path, &err) != 0) {
- yyjson_mut_val *e = fail(r, "INTERNAL", err ? err : "export failed");
- free(err);
- return e;
- }
- char hex[65];
- int64_t size = 0;
- if (hash_file(path, hex, &size) != 0)
- return fail(r, "INTERNAL", "could not read exported file");
-
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "sie.export", "{}", "OK", NULL);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_id", fy);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "path", path);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "sha256", hex);
- yyjson_mut_obj_add_int(r->rdoc, o, "size", size);
- if (inline_out) {
- size_t flen = 0;
- unsigned char *content = read_file(path, &flen);
- if (!content)
- return fail(r, "INTERNAL", "could not read exported file");
- char *b64 = util_b64(content, flen);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "content_base64", b64);
- free(b64);
- free(content);
- }
- return o;
-}
-
-static yyjson_mut_val *h_sie_import(struct req *r)
-{
- const char *b64 = arg_str(r->args, "content_base64");
- const char *path = arg_str(r->args, "path");
- unsigned char *content = NULL;
- size_t len = 0;
- if (b64) {
- if (util_b64_decode(b64, strlen(b64), &content, &len) != 0)
- return fail(r, "INVALID_ARGS",
- "content_base64 is not valid base64");
- } else if (path) {
- content = read_file(path, &len);
- if (!content)
- return fail(r, "NOT_FOUND", "could not read file");
- } else {
- return fail(r, "INVALID_ARGS", "content_base64 or path is required");
- }
- if (len > 64u * 1024u * 1024u) {
- free(content);
- return fail(r, "TOO_LARGE", "SIE file is too large");
- }
- int64_t fy = 0, vouchers = 0, accounts = 0;
- char *err = NULL;
- int rc = sie_import_content(r->db, r->org_id, r->sess->user_id, content,
- len, r->dry_run, &fy, &vouchers, &accounts,
- &err);
- free(content);
- if (rc != 0) {
- yyjson_mut_val *e = fail(r, "INVALID_ARGS", err ? err : "import failed");
- free(err);
- return e;
- }
- if (!r->dry_run) {
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "sie.import", reqjson, "OK", NULL);
- free(reqjson);
- }
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_id", fy);
- yyjson_mut_obj_add_int(r->rdoc, o, "vouchers", vouchers);
- yyjson_mut_obj_add_int(r->rdoc, o, "accounts_created", accounts);
- if (r->dry_run)
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
-}
-
-/* ------------------------------------------------------------------ */
-/* bank reconciliation (phase 1: import and match, never book) */
-/* ------------------------------------------------------------------ */
-
-struct seb_row {
- const char *booked_at;
- const char *value_date;
- const char *text;
- const char *type;
- int64_t amount_ore;
- int has_balance;
- int64_t balance_ore;
-};
-
-static int csv_split(char *line, char **fields, int max)
-{
- int nf = 0, in_quotes = 0;
- char *dst = line;
- fields[nf++] = dst;
- for (char *p = line; *p; p++) {
- if (*p == '"') {
- if (in_quotes && p[1] == '"') {
- *dst++ = '"';
- p++;
- } else {
- in_quotes = !in_quotes;
- }
- } else if (*p == ';' && !in_quotes) {
- *dst++ = '\0';
- if (nf >= max)
- return -1;
- fields[nf++] = dst;
- } else {
- *dst++ = *p;
- }
- }
- *dst = '\0';
- return in_quotes ? -1 : nf;
-}
-
-static int seb_amount(const char *s, int64_t *out)
-{
- while (*s == ' ')
- s++;
- int neg = 0;
- if (*s == '-') {
- neg = 1;
- s++;
- } else if (*s == '+') {
- s++;
- }
- int64_t whole = 0, frac = 0;
- int digits = 0, fdigits = 0, comma = 0;
- for (; *s; s++) {
- if (*s == ' ' || *s == '.')
- continue;
- if (*s == ',') {
- if (comma)
- return -1;
- comma = 1;
- continue;
- }
- if (*s < '0' || *s > '9')
- return -1;
- if (comma) {
- if (fdigits >= 2)
- return -1;
- frac = frac * 10 + (*s - '0');
- fdigits++;
- } else {
- if (whole > (INT64_MAX - 9) / 10)
- return -1;
- whole = whole * 10 + (*s - '0');
- digits++;
- }
- }
- if (digits == 0)
- return -1;
- while (fdigits < 2) {
- frac *= 10;
- fdigits++;
- }
- if (whole > (INT64_MAX - frac) / 100)
- return -1;
- int64_t v = whole * 100 + frac;
- *out = neg ? -v : v;
- return 0;
-}
-
-static int seb_parse(char *csv, struct seb_row **out, size_t *out_n,
- char *err, size_t errlen)
-{
- static const char *header[7] = {
- "Bokförd", "Valutadatum", "Text", "Typ",
- "Insättningar", "Uttag", "Bokfört saldo",
- };
- struct seb_row *rows = NULL;
- size_t n = 0, cap = 0;
- char *cur = csv;
- int lineno = 0;
- while (*cur) {
- lineno++;
- char *nl = strchr(cur, '\n');
- if (nl)
- *nl = '\0';
- size_t llen = strlen(cur);
- if (llen && cur[llen - 1] == '\r')
- cur[--llen] = '\0';
- char *fields[8];
- int nf = csv_split(cur, fields, 8);
- if (nf < 0) {
- snprintf(err, errlen, "malformed SEB CSV on line %d", lineno);
- goto bad;
- }
- if (lineno == 1) {
- int ok = nf == 7;
- for (int i = 0; ok && i < 7; i++)
- if (strcmp(fields[i], header[i]) != 0)
- ok = 0;
- if (!ok) {
- snprintf(err, errlen, "not a SEB CSV export: header mismatch");
- goto bad;
- }
- if (!nl)
- break;
- cur = nl + 1;
- continue;
- }
- if (llen == 0) {
- if (!nl)
- break;
- cur = nl + 1;
- continue;
- }
- if (nf != 7) {
- snprintf(err, errlen, "malformed SEB CSV on line %d", lineno);
- goto bad;
- }
- struct seb_row row;
- row.booked_at = fields[0];
- row.value_date = fields[1];
- row.text = fields[2];
- row.type = fields[3];
- row.has_balance = fields[6][0] != '\0';
- row.balance_ore = 0;
- int has_dep = fields[4][0] != '\0';
- int has_wd = fields[5][0] != '\0';
- if (!util_date_valid(fields[0]) || !util_date_valid(fields[1]) ||
- has_dep + has_wd != 1) {
- snprintf(err, errlen, "malformed SEB CSV on line %d", lineno);
- goto bad;
- }
- if (has_dep) {
- if (seb_amount(fields[4], &row.amount_ore) != 0) {
- snprintf(err, errlen, "malformed SEB CSV on line %d", lineno);
- goto bad;
- }
- } else {
- int64_t w = 0;
- if (seb_amount(fields[5], &w) != 0) {
- snprintf(err, errlen, "malformed SEB CSV on line %d", lineno);
- goto bad;
- }
- row.amount_ore = w > 0 ? -w : w;
- }
- if (row.has_balance && seb_amount(fields[6], &row.balance_ore) != 0) {
- snprintf(err, errlen, "malformed SEB CSV on line %d", lineno);
- goto bad;
- }
- if (n == cap) {
- cap = cap ? cap * 2 : 64;
- rows = xrealloc(rows, cap * sizeof *rows);
- }
- rows[n++] = row;
- if (!nl)
- break;
- cur = nl + 1;
- }
- *out = rows;
- *out_n = n;
- return 0;
-bad:
- free(rows);
- return -1;
-}
-
-static void bank_field(struct buf *b, const char *s, int wide)
-{
- size_t n = strlen(s);
- if (wide)
- buf_append_u32be(b, (uint32_t)n);
- else
- buf_append_u16be(b, (uint16_t)n);
- buf_append(b, s, n);
-}
-
-static void bank_row_hash(const char *account, const struct seb_row *row,
- unsigned char out[32])
-{
- static const char tag[] = "bokf-v1-bank-tx";
- struct buf b;
- buf_init(&b);
- buf_append(&b, tag, sizeof tag);
- bank_field(&b, account, 0);
- bank_field(&b, row->booked_at, 0);
- bank_field(&b, row->value_date, 0);
- bank_field(&b, row->text, 1);
- bank_field(&b, row->type, 1);
- buf_append_u64be(&b, (uint64_t)row->amount_ore);
- unsigned char flag = row->has_balance ? 1 : 0;
- buf_append(&b, &flag, 1);
- if (row->has_balance)
- buf_append_u64be(&b, (uint64_t)row->balance_ore);
- util_sha256(b.p, b.len, out);
- buf_free(&b);
-}
-
-static int bank_hash_cmp(const void *a, const void *b)
-{
- return memcmp(a, b, 32);
-}
-
-static int bank_account_exists(struct req *r, const char *account)
-{
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db, "SELECT 1 FROM accounts WHERE org_id=?1 AND number=?2", -1,
- &st, NULL) != SQLITE_OK)
- return -1;
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_text(st, 2, account, -1, SQLITE_TRANSIENT);
- int found = sqlite3_step(st) == SQLITE_ROW;
- sqlite3_finalize(st);
- return found;
-}
-
-static yyjson_mut_val *h_bank_import(struct req *r)
-{
- const char *format = arg_str(r->args, "format");
- const char *b64 = arg_str(r->args, "content_base64");
- const char *path = arg_str(r->args, "path");
- const char *account = arg_str(r->args, "account");
- if (!format || strcmp(format, "seb") != 0)
- return fail(r, "INVALID_ARGS", "format must be \"seb\"");
- unsigned char *content = NULL;
- size_t len = 0;
- if (b64) {
- if (util_b64_decode(b64, strlen(b64), &content, &len) != 0)
- return fail(r, "INVALID_ARGS",
- "content_base64 is not valid base64");
- } else if (path) {
- content = read_file(path, &len);
- if (!content)
- return fail(r, "NOT_FOUND", "could not read file");
- } else {
- return fail(r, "INVALID_ARGS", "content_base64 or path is required");
- }
- if (len > 64u * 1024u * 1024u) {
- free(content);
- return fail(r, "TOO_LARGE", "bank statement is too large");
- }
- if (memchr(content, '\0', len) != NULL) {
- free(content);
- return fail(r, "INVALID_ARGS", "not a SEB CSV export: header mismatch");
- }
- char *csv = xmalloc(len + 1);
- memcpy(csv, content, len);
- csv[len] = '\0';
- free(content);
- char *start = csv;
- if (len >= 3 && memcmp(start, "\xEF\xBB\xBF", 3) == 0)
- start += 3;
- char perr[128];
- struct seb_row *rows = NULL;
- size_t nrows = 0;
- if (seb_parse(start, &rows, &nrows, perr, sizeof perr) != 0) {
- free(csv);
- return fail(r, "INVALID_ARGS", perr);
- }
-
- char *setting = account && *account
- ? NULL
- : db_setting(r->db, r->org_id, "bank_account");
- const char *acct = account && *account
- ? account
- : (setting ? setting : "1930");
- int found = bank_account_exists(r, acct);
- if (found < 0) {
- free(setting);
- free(rows);
- free(csv);
- return fail(r, "INTERNAL", "database error");
- }
- if (!found) {
- yyjson_mut_val *res =
- failf(r, "ACCOUNT_NOT_FOUND", "account %s not found", acct);
- free(setting);
- free(rows);
- free(csv);
- return res;
- }
-
- unsigned char(*hashes)[32] = xmalloc((nrows ? nrows : 1) * 32);
- for (size_t i = 0; i < nrows; i++)
- bank_row_hash(acct, &rows[i], hashes[i]);
-
- int64_t imported = 0;
- if (r->dry_run) {
- unsigned char(*sorted)[32] = xmalloc((nrows ? nrows : 1) * 32);
- memcpy(sorted, hashes, nrows * 32);
- qsort(sorted, nrows, 32, bank_hash_cmp);
- for (size_t i = 0; i < nrows;) {
- size_t j = i + 1;
- while (j < nrows && memcmp(sorted[i], sorted[j], 32) == 0)
- j++;
- sqlite3_stmt *q = NULL;
- int exists = 0;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT 1 FROM bank_transactions WHERE org_id=?1"
- " AND source_hash=?2",
- -1, &q, NULL) != SQLITE_OK) {
- free(sorted);
- free(hashes);
- free(setting);
- free(rows);
- free(csv);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(q, 1, r->org_id);
- sqlite3_bind_blob(q, 2, sorted[i], 32, SQLITE_TRANSIENT);
- exists = sqlite3_step(q) == SQLITE_ROW;
- sqlite3_finalize(q);
- if (!exists)
- imported++;
- i = j;
- }
- free(sorted);
- } else {
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- int failed = db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0;
- for (size_t i = 0; i < nrows && !failed; i++) {
- sqlite3_stmt *ins = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO bank_transactions(org_id,account,booked_at,"
- "value_date,text,type,amount_ore,balance_ore,source,"
- "source_hash,imported_at,imported_by)"
- " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,'seb-csv',?9,?10,?11)"
- " ON CONFLICT(org_id,source_hash) DO NOTHING",
- -1, &ins, NULL) != SQLITE_OK) {
- failed = 1;
- break;
- }
- sqlite3_bind_int64(ins, 1, r->org_id);
- sqlite3_bind_text(ins, 2, acct, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(ins, 3, rows[i].booked_at, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(ins, 4, rows[i].value_date, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(ins, 5, rows[i].text, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(ins, 6, rows[i].type, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(ins, 7, rows[i].amount_ore);
- if (rows[i].has_balance)
- sqlite3_bind_int64(ins, 8, rows[i].balance_ore);
- else
- sqlite3_bind_null(ins, 8);
- sqlite3_bind_blob(ins, 9, hashes[i], 32, SQLITE_TRANSIENT);
- sqlite3_bind_text(ins, 10, ts, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(ins, 11, r->sess->user_id);
- int rc = sqlite3_step(ins);
- sqlite3_finalize(ins);
- if (rc != SQLITE_DONE) {
- failed = 1;
- break;
- }
- if (sqlite3_changes(r->db) > 0)
- imported++;
- }
- if (failed) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(hashes);
- free(setting);
- free(rows);
- free(csv);
- return fail(r, "INTERNAL", "database error");
- }
- if (db_exec(r->db, "COMMIT", NULL) != 0) {
- db_exec(r->db, "ROLLBACK", NULL);
- free(hashes);
- free(setting);
- free(rows);
- free(csv);
- return fail(r, "INTERNAL", "database error");
- }
- }
- int64_t duplicates = (int64_t)nrows - imported;
- const char *first = NULL, *last = NULL;
- for (size_t i = 0; i < nrows; i++) {
- if (!first || strcmp(rows[i].booked_at, first) < 0)
- first = rows[i].booked_at;
- if (!last || strcmp(rows[i].booked_at, last) > 0)
- last = rows[i].booked_at;
- }
- if (!r->dry_run) {
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "bank.import", reqjson, "OK", NULL);
- free(reqjson);
- }
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "format", "seb");
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "account", acct);
- yyjson_mut_obj_add_int(r->rdoc, o, "total", (int64_t)nrows);
- yyjson_mut_obj_add_int(r->rdoc, o, "imported", imported);
- yyjson_mut_obj_add_int(r->rdoc, o, "duplicates", duplicates);
- if (first)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "first_date", first);
- else
- yyjson_mut_obj_add_null(r->rdoc, o, "first_date");
- if (last)
- yyjson_mut_obj_add_strcpy(r->rdoc, o, "last_date", last);
- else
- yyjson_mut_obj_add_null(r->rdoc, o, "last_date");
- if (r->dry_run)
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- free(hashes);
- free(setting);
- free(rows);
- free(csv);
- return o;
-}
-
-static yyjson_mut_val *h_bank_list(struct req *r)
-{
- const char *status = arg_str(r->args, "status");
- const char *from = arg_str(r->args, "from");
- const char *to = arg_str(r->args, "to");
- const char *account = arg_str(r->args, "account");
- int64_t limit = 200;
- arg_int(r->args, "limit", &limit);
- if (limit < 1)
- limit = 200;
- if (limit > 1000)
- limit = 1000;
- int status_code = 0;
- if (status && strcmp(status, "unmatched") == 0)
- status_code = 1;
- else if (status && strcmp(status, "matched") == 0)
- status_code = 2;
-
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT t.id,t.account,t.booked_at,t.value_date,t.text,t.type,"
- "t.amount_ore,t.balance_ore FROM bank_transactions t"
- " WHERE t.org_id=?1"
- " AND (?2 IS NULL OR t.account=?2)"
- " AND (?3 IS NULL OR t.booked_at>=?3)"
- " AND (?4 IS NULL OR t.booked_at<=?4)"
- " AND (?5=0"
- " OR (?5=1 AND NOT EXISTS (SELECT 1 FROM bank_matches m"
- " WHERE m.org_id=t.org_id AND m.transaction_id=t.id))"
- " OR (?5=2 AND EXISTS (SELECT 1 FROM bank_matches m"
- " WHERE m.org_id=t.org_id AND m.transaction_id=t.id)))"
- " ORDER BY t.booked_at,t.id LIMIT ?6",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- if (account)
- sqlite3_bind_text(st, 2, account, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 2);
- if (from)
- sqlite3_bind_text(st, 3, from, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 3);
- if (to)
- sqlite3_bind_text(st, 4, to, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(st, 4);
- sqlite3_bind_int64(st, 5, status_code);
- sqlite3_bind_int64(st, 6, limit);
-
- yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
- while (sqlite3_step(st) == SQLITE_ROW) {
- int64_t tx_id = sqlite3_column_int64(st, 0);
- char tx_account[16];
- snprintf(tx_account, sizeof tx_account, "%s",
- sq(sqlite3_column_text(st, 1)));
- const char *booked_at = sq(sqlite3_column_text(st, 2));
- yyjson_mut_val *item = yyjson_mut_arr_add_obj(r->rdoc, items);
- yyjson_mut_obj_add_int(r->rdoc, item, "id", tx_id);
- yyjson_mut_obj_add_strcpy(r->rdoc, item, "account", tx_account);
- yyjson_mut_obj_add_strcpy(r->rdoc, item, "booked_at", booked_at);
- yyjson_mut_obj_add_strcpy(r->rdoc, item, "value_date",
- sq(sqlite3_column_text(st, 3)));
- yyjson_mut_obj_add_strcpy(r->rdoc, item, "text",
- sq(sqlite3_column_text(st, 4)));
- yyjson_mut_obj_add_strcpy(r->rdoc, item, "type",
- sq(sqlite3_column_text(st, 5)));
- yyjson_mut_obj_add_int(r->rdoc, item, "amount_ore",
- sqlite3_column_int64(st, 6));
- if (sqlite3_column_type(st, 7) == SQLITE_NULL)
- yyjson_mut_obj_add_null(r->rdoc, item, "balance_ore");
- else
- yyjson_mut_obj_add_int(r->rdoc, item, "balance_ore",
- sqlite3_column_int64(st, 7));
-
- yyjson_mut_val *matches = yyjson_mut_arr(r->rdoc);
- sqlite3_stmt *ms = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT v.id,v.series,v.number,v.date,"
- "COALESCE(SUM(r.debit_ore-r.credit_ore),0)"
- " FROM bank_matches m"
- " JOIN vouchers v ON v.org_id=m.org_id AND v.id=m.voucher_id"
- " LEFT JOIN accounts a ON a.org_id=v.org_id AND a.number=?2"
- " LEFT JOIN voucher_rows r ON r.org_id=v.org_id"
- " AND r.voucher_id=v.id AND r.account_id=a.id"
- " WHERE m.org_id=?1 AND m.transaction_id=?3"
- " GROUP BY v.id,v.series,v.number,v.date ORDER BY v.id",
- -1, &ms, NULL) != SQLITE_OK) {
- sqlite3_finalize(st);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(ms, 1, r->org_id);
- sqlite3_bind_text(ms, 2, tx_account, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(ms, 3, tx_id);
- while (sqlite3_step(ms) == SQLITE_ROW) {
- yyjson_mut_val *m = yyjson_mut_arr_add_obj(r->rdoc, matches);
- yyjson_mut_obj_add_int(r->rdoc, m, "voucher_id",
- sqlite3_column_int64(ms, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, m, "series",
- sq(sqlite3_column_text(ms, 1)));
- yyjson_mut_obj_add_int(r->rdoc, m, "number",
- sqlite3_column_int64(ms, 2));
- yyjson_mut_obj_add_strcpy(r->rdoc, m, "date",
- sq(sqlite3_column_text(ms, 3)));
- yyjson_mut_obj_add_int(r->rdoc, m, "bank_amount_ore",
- sqlite3_column_int64(ms, 4));
- }
- sqlite3_finalize(ms);
- yyjson_mut_obj_add_val(r->rdoc, item, "matches", matches);
-
- yyjson_mut_val *suggestions = yyjson_mut_arr(r->rdoc);
- if (yyjson_mut_arr_size(matches) == 0) {
- sqlite3_stmt *ss = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT v.id,v.series,v.number,v.date,"
- "SUM(r.debit_ore-r.credit_ore)"
- " FROM vouchers v"
- " JOIN voucher_rows r ON r.org_id=v.org_id"
- " AND r.voucher_id=v.id"
- " JOIN accounts a ON a.org_id=r.org_id"
- " AND a.id=r.account_id"
- " WHERE v.org_id=?1 AND a.number=?2"
- " AND ABS(julianday(v.date)-julianday(?3))<=5"
- " AND NOT EXISTS (SELECT 1 FROM bank_matches m"
- " WHERE m.org_id=v.org_id AND m.voucher_id=v.id)"
- " GROUP BY v.id,v.series,v.number,v.date"
- " HAVING SUM(r.debit_ore-r.credit_ore)=?4"
- " ORDER BY ABS(julianday(v.date)-julianday(?3)),v.date,"
- "v.id LIMIT 3",
- -1, &ss, NULL) != SQLITE_OK) {
- sqlite3_finalize(st);
- return fail(r, "INTERNAL", "database error");
- }
- sqlite3_bind_int64(ss, 1, r->org_id);
- sqlite3_bind_text(ss, 2, tx_account, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(ss, 3, booked_at, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(ss, 4, sqlite3_column_int64(st, 6));
- while (sqlite3_step(ss) == SQLITE_ROW) {
- yyjson_mut_val *s =
- yyjson_mut_arr_add_obj(r->rdoc, suggestions);
- yyjson_mut_obj_add_int(r->rdoc, s, "voucher_id",
- sqlite3_column_int64(ss, 0));
- yyjson_mut_obj_add_strcpy(r->rdoc, s, "series",
- sq(sqlite3_column_text(ss, 1)));
- yyjson_mut_obj_add_int(r->rdoc, s, "number",
- sqlite3_column_int64(ss, 2));
- yyjson_mut_obj_add_strcpy(r->rdoc, s, "date",
- sq(sqlite3_column_text(ss, 3)));
- yyjson_mut_obj_add_int(r->rdoc, s, "amount_ore",
- sqlite3_column_int64(ss, 4));
- }
- sqlite3_finalize(ss);
- }
- yyjson_mut_obj_add_val(r->rdoc, item, "suggestions", suggestions);
- }
- sqlite3_finalize(st);
-
- sqlite3_stmt *sum = NULL;
- int64_t unmatched = 0, matched = 0, unmatched_amount = 0;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT COUNT(*),"
- "COALESCE(SUM(EXISTS (SELECT 1 FROM bank_matches m"
- " WHERE m.org_id=t.org_id AND m.transaction_id=t.id)),0),"
- "COALESCE(SUM(CASE WHEN EXISTS (SELECT 1 FROM bank_matches m"
- " WHERE m.org_id=t.org_id AND m.transaction_id=t.id)"
- " THEN 0 ELSE t.amount_ore END),0)"
- " FROM bank_transactions t"
- " WHERE t.org_id=?1 AND (?2 IS NULL OR t.account=?2)",
- -1, &sum, NULL) == SQLITE_OK) {
- sqlite3_bind_int64(sum, 1, r->org_id);
- if (account)
- sqlite3_bind_text(sum, 2, account, -1, SQLITE_TRANSIENT);
- else
- sqlite3_bind_null(sum, 2);
- if (sqlite3_step(sum) == SQLITE_ROW) {
- int64_t total = sqlite3_column_int64(sum, 0);
- matched = sqlite3_column_int64(sum, 1);
- unmatched = total - matched;
- unmatched_amount = sqlite3_column_int64(sum, 2);
- }
- sqlite3_finalize(sum);
- }
- yyjson_mut_val *res = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_val(r->rdoc, res, "items", items);
- yyjson_mut_val *s = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, s, "unmatched", unmatched);
- yyjson_mut_obj_add_int(r->rdoc, s, "matched", matched);
- yyjson_mut_obj_add_int(r->rdoc, s, "unmatched_amount_ore",
- unmatched_amount);
- yyjson_mut_obj_add_val(r->rdoc, res, "summary", s);
- return res;
-}
-
-static int bank_legs_sum(sqlite3 *db, int64_t org_id, int64_t tx_id,
- const char *account, int64_t *out)
-{
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- db,
- "SELECT COALESCE(SUM(r.debit_ore-r.credit_ore),0)"
- " FROM bank_matches m"
- " JOIN voucher_rows r ON r.org_id=m.org_id"
- " AND r.voucher_id=m.voucher_id"
- " JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
- " WHERE m.org_id=?1 AND m.transaction_id=?2 AND a.number=?3",
- -1, &st, NULL) != SQLITE_OK)
- return -1;
- sqlite3_bind_int64(st, 1, org_id);
- sqlite3_bind_int64(st, 2, tx_id);
- sqlite3_bind_text(st, 3, account, -1, SQLITE_TRANSIENT);
- int rc = sqlite3_step(st);
- if (rc == SQLITE_ROW)
- *out = sqlite3_column_int64(st, 0);
- sqlite3_finalize(st);
- return rc == SQLITE_ROW ? 0 : -1;
-}
-
-static yyjson_mut_val *h_bank_match(struct req *r)
-{
- int64_t tx_id = 0, voucher_id = 0;
- if (!arg_int(r->args, "transaction_id", &tx_id) || tx_id <= 0 ||
- !arg_int(r->args, "voucher_id", &voucher_id) || voucher_id <= 0)
- return fail(r, "INVALID_ARGS",
- "transaction_id and voucher_id are required");
- sqlite3_stmt *st = NULL;
- char account[16];
- int64_t amount = 0;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT account,amount_ore FROM bank_transactions"
- " 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, tx_id);
- if (sqlite3_step(st) != SQLITE_ROW) {
- sqlite3_finalize(st);
- return fail(r, "NOT_FOUND", "bank transaction not found");
- }
- snprintf(account, sizeof account, "%s", sq(sqlite3_column_text(st, 0)));
- amount = sqlite3_column_int64(st, 1);
- sqlite3_finalize(st);
-
- int found = 0;
- if (sqlite3_prepare_v2(
- r->db, "SELECT 1 FROM vouchers 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, voucher_id);
- found = sqlite3_step(st) == SQLITE_ROW;
- sqlite3_finalize(st);
- if (!found)
- return fail(r, "NOT_FOUND", "voucher not found");
-
- int touches = 0;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT 1 FROM voucher_rows r"
- " JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
- " WHERE r.org_id=?1 AND r.voucher_id=?2 AND a.number=?3",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, voucher_id);
- sqlite3_bind_text(st, 3, account, -1, SQLITE_TRANSIENT);
- touches = sqlite3_step(st) == SQLITE_ROW;
- sqlite3_finalize(st);
- if (!touches)
- return failf(r, "INVALID_ARGS",
- "voucher does not post to account %s", account);
-
- int linked = 0;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT 1 FROM bank_matches WHERE org_id=?1 AND transaction_id=?2"
- " AND voucher_id=?3",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, tx_id);
- sqlite3_bind_int64(st, 3, voucher_id);
- linked = sqlite3_step(st) == SQLITE_ROW;
- sqlite3_finalize(st);
- if (linked)
- return fail(r, "CONFLICT",
- "transaction is already matched to this voucher");
-
- int64_t legs = 0;
- if (bank_legs_sum(r->db, r->org_id, tx_id, account, &legs) != 0)
- return fail(r, "INTERNAL", "database error");
- if (r->dry_run) {
- sqlite3_stmt *vs = NULL;
- int64_t voucher_leg = 0;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT COALESCE(SUM(r.debit_ore-r.credit_ore),0)"
- " FROM voucher_rows r"
- " JOIN accounts a ON a.org_id=r.org_id AND a.id=r.account_id"
- " WHERE r.org_id=?1 AND r.voucher_id=?2 AND a.number=?3",
- -1, &vs, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(vs, 1, r->org_id);
- sqlite3_bind_int64(vs, 2, voucher_id);
- sqlite3_bind_text(vs, 3, account, -1, SQLITE_TRANSIENT);
- if (sqlite3_step(vs) == SQLITE_ROW)
- voucher_leg = sqlite3_column_int64(vs, 0);
- sqlite3_finalize(vs);
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "transaction_id", tx_id);
- yyjson_mut_obj_add_int(r->rdoc, o, "voucher_id", voucher_id);
- yyjson_mut_obj_add_int(r->rdoc, o, "difference_ore",
- amount - (legs + voucher_leg));
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
- }
-
- char ts[32];
- util_iso8601(util_now(), ts, sizeof ts);
- if (sqlite3_prepare_v2(
- r->db,
- "INSERT INTO bank_matches(org_id,transaction_id,voucher_id,"
- "matched_at,matched_by,kind) VALUES(?1,?2,?3,?4,?5,'manual')",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, tx_id);
- sqlite3_bind_int64(st, 3, voucher_id);
- sqlite3_bind_text(st, 4, ts, -1, SQLITE_TRANSIENT);
- sqlite3_bind_int64(st, 5, r->sess->user_id);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE)
- return fail(r, "INTERNAL", "database error");
-
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "bank.match", reqjson, "OK", NULL);
- free(reqjson);
-
- if (bank_legs_sum(r->db, r->org_id, tx_id, account, &legs) != 0)
- return fail(r, "INTERNAL", "database error");
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "transaction_id", tx_id);
- yyjson_mut_obj_add_int(r->rdoc, o, "voucher_id", voucher_id);
- yyjson_mut_obj_add_int(r->rdoc, o, "difference_ore", amount - legs);
- return o;
-}
-
-static yyjson_mut_val *h_bank_unmatch(struct req *r)
-{
- int64_t tx_id = 0, voucher_id = 0;
- if (!arg_int(r->args, "transaction_id", &tx_id) || tx_id <= 0 ||
- !arg_int(r->args, "voucher_id", &voucher_id) || voucher_id <= 0)
- return fail(r, "INVALID_ARGS",
- "transaction_id and voucher_id are required");
- sqlite3_stmt *st = NULL;
- if (sqlite3_prepare_v2(
- r->db,
- "SELECT 1 FROM bank_matches WHERE org_id=?1 AND transaction_id=?2"
- " AND voucher_id=?3",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, tx_id);
- sqlite3_bind_int64(st, 3, voucher_id);
- int found = sqlite3_step(st) == SQLITE_ROW;
- sqlite3_finalize(st);
- if (!found)
- return fail(r, "NOT_FOUND", "match not found");
- if (!r->dry_run) {
- if (sqlite3_prepare_v2(
- r->db,
- "DELETE FROM bank_matches WHERE org_id=?1"
- " AND transaction_id=?2 AND voucher_id=?3",
- -1, &st, NULL) != SQLITE_OK)
- return fail(r, "INTERNAL", "database error");
- sqlite3_bind_int64(st, 1, r->org_id);
- sqlite3_bind_int64(st, 2, tx_id);
- sqlite3_bind_int64(st, 3, voucher_id);
- int rc = sqlite3_step(st);
- sqlite3_finalize(st);
- if (rc != SQLITE_DONE)
- return fail(r, "INTERNAL", "database error");
- if (sqlite3_changes(r->db) == 0)
- return fail(r, "NOT_FOUND", "match not found");
- char *reqjson = audit_args_json(r->args);
- audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
- "bank.unmatch", reqjson, "OK", NULL);
- free(reqjson);
- }
- yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- yyjson_mut_obj_add_int(r->rdoc, o, "transaction_id", tx_id);
- yyjson_mut_obj_add_int(r->rdoc, o, "voucher_id", voucher_id);
- yyjson_mut_obj_add_bool(r->rdoc, o, "unmatched", true);
- if (r->dry_run)
- yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
- return o;
-}
-
-/* command table */
-/* ------------------------------------------------------------------ */
-
-static const struct cmd_arg args_session_open[] = {
- { "method", ARG_ENUM, 1, NULL, "password,token",
- "Open with a password or an API token" },
- { "username", ARG_STR, 0, NULL, NULL, "Username for method=password" },
- { "password", ARG_STR, 0, NULL, NULL, "Password for method=password" },
- { "token", ARG_STR, 0, NULL, NULL, "API token for method=token" },
-};
-
-static const struct cmd_arg args_session_use_org[] = {
- { "org", ARG_INT, 1, NULL, NULL, "Org id" },
-};
-
-static const struct cmd_arg args_org_create[] = {
- { "name", ARG_STR, 1, NULL, NULL, "Org name" },
- { "org_nr", ARG_STR, 0, NULL, NULL, "Swedish org number" },
- { "fiscal_year_start_month", ARG_INT, 0, "1", NULL,
- "Fiscal year start month 1-12" },
- { "moms_period", ARG_ENUM, 0, "month", "month,quarter,year",
- "VAT reporting period" },
- { "framework", ARG_ENUM, 0, "K2", "K2,K3", "Accounting framework" },
-};
-
-static const struct cmd_arg args_org_update[] = {
- { "name", ARG_STR, 0, NULL, NULL, "Org name" },
- { "org_nr", ARG_STR, 0, NULL, NULL, "Swedish org number" },
- { "vat_nr", ARG_STR, 0, NULL, NULL, "VAT number" },
- { "address", ARG_STR, 0, NULL, NULL, "Street address" },
- { "postal_code", ARG_STR, 0, NULL, NULL, "Postal code" },
- { "city", ARG_STR, 0, NULL, NULL, "City" },
- { "country", ARG_STR, 0, NULL, NULL, "Country" },
- { "email", ARG_STR, 0, NULL, NULL, "E-mail" },
- { "phone", ARG_STR, 0, NULL, NULL, "Phone" },
- { "moms_period", ARG_ENUM, 0, NULL, "month,quarter,year",
- "VAT reporting period" },
- { "framework", ARG_ENUM, 0, NULL, "K2,K3", "Accounting framework" },
- { "description", ARG_STR, 0, NULL, NULL, "Free-text description" },
- { "fiscal_year_start_month", ARG_INT, 0, NULL, NULL,
- "Fiscal year start month 1-12" },
- { "shares", ARG_INT, 0, NULL, NULL, "Number of shares" },
-};
-
-static const struct cmd_arg args_member_role[] = {
- { "username", ARG_STR, 1, NULL, NULL, "Username" },
- { "role", ARG_ENUM, 1, NULL, "owner,bookkeeper,viewer",
- "Membership role" },
-};
-
-static const struct cmd_arg args_member_username[] = {
- { "username", ARG_STR, 1, NULL, NULL, "Username" },
-};
-
-static const struct cmd_arg args_board_add[] = {
- { "name", ARG_STR, 1, NULL, NULL, "Board member name" },
- { "title", ARG_STR, 0, "Styrelseledamot", NULL, "Role on the board" },
-};
-
-static const struct cmd_arg args_board_update[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Board member id" },
- { "name", ARG_STR, 0, NULL, NULL, "Board member name" },
- { "title", ARG_STR, 0, NULL, NULL, "Role on the board" },
-};
-
-static const struct cmd_arg args_board_remove[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Board member id" },
-};
-
-static const struct cmd_arg args_user_create[] = {
- { "username", ARG_STR, 1, NULL, NULL, "Username" },
- { "password", ARG_STR, 1, NULL, NULL, "Password" },
- { "display_name", ARG_STR, 0, NULL, NULL, "Display name" },
- { "is_admin", ARG_BOOL, 0, "false", NULL, "System admin" },
-};
-
-static const struct cmd_arg args_token_create[] = {
- { "label", ARG_STR, 1, NULL, NULL, "Label shown in token.list" },
- { "scopes", ARG_JSON, 0, NULL, NULL, "Array of read, write, admin" },
- { "expires_at", ARG_DATE, 0, NULL, NULL, "Expiry date" },
-};
-
-static const struct cmd_arg args_token_revoke[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Token id" },
-};
-
static const struct cmd_arg args_describe[] = {
{ "cmd", ARG_STR, 0, NULL, NULL,
"Command name; omit for the full catalogue" },
};
-static const struct cmd_arg args_audit_list[] = {
- { "cursor", ARG_INT, 0, NULL, NULL, "Cursor from next_cursor" },
- { "limit", ARG_INT, 0, "100", NULL, "Page size, 1-1000" },
- { "action", ARG_STR, 0, NULL, NULL, "Filter by action" },
-};
-
-static const struct cmd_arg args_audit_verify[] = {
- { "full", ARG_BOOL, 0, "false", NULL, "Also re-hash attachments" },
-};
-
-static const struct cmd_arg args_backup_snapshot[] = {
- { "dest", ARG_STR, 0, NULL, NULL,
- "Destination path; defaults to backup_dir with a timestamp" },
-};
-
-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" },
-};
-
-static const struct cmd_arg args_fiscal_year_get[] = {
- { "id", ARG_INT, 0, NULL, NULL,
- "Fiscal year id; defaults to the latest" },
-};
-
-static const struct cmd_arg args_fiscal_year_open[] = {
- { "label", ARG_STR, 1, NULL, NULL, "Fiscal year label" },
- { "start_date", ARG_DATE, 1, NULL, NULL, "Start date (YYYY-MM-DD)" },
- { "end_date", ARG_DATE, 1, NULL, NULL, "End date (YYYY-MM-DD)" },
-};
-
-static const struct cmd_arg args_fiscal_year_close[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
- { "confirm", ARG_BOOL, 1, NULL, NULL, "Must be true" },
-};
-
-static const struct cmd_arg args_fiscal_year_reopen[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
- { "confirm", ARG_BOOL, 1, NULL, NULL, "Must be true" },
-};
-
-static const struct cmd_arg args_fiscal_year_update[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
- { "dividend_ore", ARG_INT, 0, NULL, NULL, "Proposed dividend in öre" },
- { "events", ARG_STR, 0, NULL, NULL, "Material events during the year" },
- { "agm_date", ARG_STR, 0, NULL, NULL, "AGM date; empty clears" },
- { "dividend_date", ARG_STR, 0, NULL, NULL,
- "Dividend payment date; empty clears" },
- { "employees", ARG_STR, 0, NULL, NULL, "Average number of employees" },
- { "notes", ARG_STR, 0, NULL, NULL, "Other notes" },
-};
-
-static const struct cmd_arg args_period_lock[] = {
- { "fiscal_year", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
- { "until", ARG_DATE, 1, NULL, NULL, "Lock through this date, inclusive" },
- { "reason", ARG_STR, 0, NULL, NULL, "Reason, recorded in the audit log" },
-};
-
-static const struct cmd_arg args_period_unlock[] = {
- { "fiscal_year", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
- { "reason", ARG_STR, 0, NULL, NULL, "Reason, recorded in the audit log" },
-};
-
-static const struct cmd_arg args_voucher_post[] = {
- { "date", ARG_DATE, 1, NULL, NULL, "Voucher date (YYYY-MM-DD)" },
- { "description", ARG_STR, 0, NULL, NULL, "Voucher text" },
- { "series", ARG_STR, 0, NULL, NULL,
- "Number series; defaults to settings.default_series" },
- { "client_ref", ARG_STR, 0, NULL, NULL,
- "Idempotency key, unique per org" },
- { "corrects_voucher", ARG_INT, 0, NULL, NULL,
- "Voucher id this one corrects" },
- { "rows", ARG_JSON, 0, NULL, NULL,
- "Array of {account,debit_ore,credit_ore,description?}" },
- { "template", ARG_JSON, 0, NULL, NULL,
- "Template id or name; alternative to rows" },
- { "x", ARG_JSON, 0, NULL, NULL, "Template variable in kronor" },
- { "attachment_ids", ARG_JSON, 0, NULL, NULL,
- "Array of attachment ids to link" },
-};
-
-static const struct cmd_arg args_voucher_get[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Voucher id" },
-};
-
-static const struct cmd_arg args_voucher_list[] = {
- { "fiscal_year", ARG_INT, 0, NULL, NULL,
- "Fiscal year id; defaults to the latest" },
- { "from", ARG_DATE, 0, NULL, NULL, "Earliest date" },
- { "to", ARG_DATE, 0, NULL, NULL, "Latest date" },
- { "series", ARG_STR, 0, NULL, NULL, "Number series filter" },
- { "account", ARG_STR, 0, NULL, NULL, "Account number filter" },
- { "text", ARG_STR, 0, NULL, NULL, "Description substring filter" },
- { "cursor", ARG_INT, 0, NULL, NULL, "Cursor from next_cursor" },
- { "limit", ARG_INT, 0, "100", NULL, "Page size, 1-1000" },
-};
-
-static const struct cmd_arg args_voucher_correct[] = {
- { "voucher", ARG_INT, 1, NULL, NULL, "Voucher id to correct" },
- { "description", ARG_STR, 1, NULL, NULL, "Correction text" },
- { "date", ARG_DATE, 0, NULL, NULL, "Defaults to the original date" },
- { "client_ref", ARG_STR, 0, NULL, NULL, "Idempotency key" },
-};
-
-static const struct cmd_arg args_bokslut_post[] = {
- { "fiscal_year", ARG_INT, 0, NULL, NULL,
- "Fiscal year id; defaults to the latest" },
- { "entries", ARG_JSON, 0, NULL, NULL,
- "Array of {debit_account,credit_account,amount_ore,description?}" },
- { "periodiseringsfond_ore", ARG_INT, 0, NULL, NULL,
- "Periodiseringsfond in öre" },
- { "tax_rate", ARG_JSON, 0, "20.6", NULL, "Tax rate percent, 0-100" },
- { "dispose", ARG_BOOL, 0, "true", NULL,
- "Post the result transfer to 8999/2099" },
- { "date", ARG_DATE, 0, NULL, NULL,
- "Posting date; defaults to the fiscal year end" },
-};
-
-static const struct cmd_arg args_settings_set[] = {
- { "key", ARG_STR, 1, NULL, NULL,
- "default_series, attachment_dir, bank_account,"
- " invoice_receivable_account, invoice_revenue_account, smtp_host,"
- " smtp_port, smtp_user, smtp_from, smtp_reply_to, smtp_security or"
- " smtp_password" },
- { "value", ARG_STR, 0, NULL, NULL,
- "Setting value; an empty value clears smtp_password" },
-};
-
-static const struct cmd_arg args_bank_import[] = {
- { "format", ARG_ENUM, 1, NULL, "seb", "Statement format" },
- { "content_base64", ARG_STR, 0, NULL, NULL,
- "Statement content, base64; alternative to path" },
- { "path", ARG_STR, 0, NULL, NULL,
- "Server-side path; alternative to content_base64" },
- { "account", ARG_STR, 0, NULL, NULL,
- "Bank account number; defaults to settings.bank_account" },
-};
-
-static const struct cmd_arg args_bank_list[] = {
- { "status", ARG_ENUM, 0, "all", "all,unmatched,matched",
- "Match status filter" },
- { "from", ARG_DATE, 0, NULL, NULL, "Earliest booked date" },
- { "to", ARG_DATE, 0, NULL, NULL, "Latest booked date" },
- { "account", ARG_STR, 0, NULL, NULL, "Bank account number filter" },
- { "limit", ARG_INT, 0, "200", NULL, "Page size, 1-1000" },
-};
-
-static const struct cmd_arg args_bank_match[] = {
- { "transaction_id", ARG_INT, 1, NULL, NULL, "Bank transaction id" },
- { "voucher_id", ARG_INT, 1, NULL, NULL, "Voucher id" },
-};
-
-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" },
-};
-
-static const struct cmd_arg args_template_list[] = {
- { "active_only", ARG_BOOL, 0, NULL, NULL, "Only active templates" },
-};
-
-static const struct cmd_arg args_template_get[] = {
- { "id", ARG_INT, 0, NULL, NULL, "Template id" },
- { "name", ARG_STR, 0, NULL, NULL, "Template name" },
-};
-
-static const struct cmd_arg args_template_create[] = {
- { "name", ARG_STR, 1, NULL, NULL, "Template name" },
- { "series", ARG_STR, 0, NULL, NULL,
- "Number series; defaults to settings.default_series" },
- { "description", ARG_STR, 0, NULL, NULL,
- "Voucher text; {x} is replaced with the amount" },
- { "rows", ARG_JSON, 1, NULL, NULL,
- "Array of {account,formula,description?}" },
-};
-
-static const struct cmd_arg args_template_update[] = {
- { "id", ARG_INT, 0, NULL, NULL, "Template id" },
- { "name", ARG_STR, 0, NULL, NULL,
- "Template name, or the new name when id is given" },
- { "series", ARG_STR, 0, NULL, NULL, "Number series" },
- { "description", ARG_STR, 0, NULL, NULL, "Voucher text" },
- { "active", ARG_BOOL, 0, NULL, NULL, "Active flag" },
- { "rows", ARG_JSON, 0, NULL, NULL,
- "Replacement rows, {account,formula,description?}" },
-};
-
-static const struct cmd_arg args_template_archive[] = {
- { "id", ARG_INT, 0, NULL, NULL, "Template id" },
- { "name", ARG_STR, 0, NULL, NULL, "Template name" },
-};
-
-static const struct cmd_arg args_attachment_put[] = {
- { "filename", ARG_STR, 1, NULL, NULL, "File name" },
- { "mime", ARG_STR, 0, "application/octet-stream", NULL, "MIME type" },
- { "content_base64", ARG_STR, 1, NULL, NULL, "File content, base64" },
- { "voucher_id", ARG_INT, 0, NULL, NULL, "Link to this voucher" },
-};
-
-static const struct cmd_arg args_attachment_link[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Attachment id" },
- { "voucher_id", ARG_INT, 1, NULL, NULL, "Voucher id" },
-};
-
-static const struct cmd_arg args_attachment_unlink[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Attachment id" },
- { "voucher_id", ARG_INT, 1, NULL, NULL, "Voucher id" },
-};
-
-static const struct cmd_arg args_attachment_get[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Attachment id" },
-};
-
-static const struct cmd_arg args_attachment_list[] = {
- { "voucher_id", ARG_INT, 0, NULL, NULL,
- "Only attachments on this voucher" },
- { "unlinked", ARG_BOOL, 0, NULL, NULL,
- "Only unlinked attachments (inbox)" },
- { "cursor", ARG_INT, 0, NULL, NULL, "Cursor from next_cursor" },
- { "limit", ARG_INT, 0, "100", NULL, "Page size, 1-1000" },
-};
-
-static const struct cmd_arg args_report_trial_balance[] = {
- { "fiscal_year", ARG_INT, 0, NULL, NULL,
- "Fiscal year id; defaults to the latest" },
- { "from", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
- { "to", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
- { "include_zero", ARG_BOOL, 0, "false", NULL,
- "Include accounts with no activity" },
-};
-
-static const struct cmd_arg args_report_income_statement[] = {
- { "fiscal_year", ARG_INT, 0, NULL, NULL,
- "Fiscal year id; defaults to the latest" },
- { "from", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
- { "to", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
-};
-
-static const struct cmd_arg args_report_balance_sheet[] = {
- { "fiscal_year", ARG_INT, 0, NULL, NULL,
- "Fiscal year id; defaults to the latest" },
- { "to", ARG_DATE, 0, NULL, NULL, "Report through this date" },
-};
-
-static const struct cmd_arg args_report_vat[] = {
- { "from", ARG_DATE, 1, NULL, NULL, "Period start" },
- { "to", ARG_DATE, 1, NULL, NULL, "Period end" },
-};
-
-static const struct cmd_arg args_report_general_ledger[] = {
- { "fiscal_year", ARG_INT, 0, NULL, NULL,
- "Fiscal year id; defaults to the latest" },
- { "accounts", ARG_JSON, 0, NULL, NULL,
- "Array of account numbers to include" },
- { "from", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
- { "to", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
-};
-
-static const struct cmd_arg args_report_voucher_list[] = {
- { "fiscal_year", ARG_INT, 0, NULL, NULL,
- "Fiscal year id; defaults to the latest" },
- { "series", ARG_STR, 0, NULL, NULL, "Number series filter" },
-};
-
-static const struct cmd_arg args_report_vat_eskd[] = {
- { "from", ARG_DATE, 1, NULL, NULL, "Period start" },
- { "to", ARG_DATE, 1, NULL, NULL, "Period end" },
- { "upplysning", ARG_STR, 0, NULL, NULL,
- "Free-text message to Skatteverket" },
-};
-
-static const struct cmd_arg args_sru_export[] = {
- { "fiscal_year", ARG_INT, 0, NULL, NULL,
- "Fiscal year id; defaults to the latest" },
- { "adjustments", ARG_JSON, 0, NULL, NULL,
- "Array of {code,amount_ore} tax adjustments" },
- { "submitter", ARG_JSON, 0, NULL, NULL,
- "Object overriding the INFO.SRU submitter" },
- { "assisted", ARG_BOOL, 0, NULL, NULL, "Assisted declaration" },
- { "audited", ARG_BOOL, 0, NULL, NULL, "Audited declaration" },
- { "ignore_unmapped", ARG_BOOL, 0, "false", NULL,
- "Proceed despite unmapped accounts" },
-};
-
-static const struct cmd_arg args_sie_export[] = {
- { "fiscal_year", ARG_INT, 0, NULL, NULL,
- "Fiscal year id; defaults to the latest" },
- { "inline", ARG_BOOL, 0, "false", NULL, "Also return content_base64" },
-};
-
-static const struct cmd_arg args_sie_import[] = {
- { "content_base64", ARG_STR, 0, NULL, NULL, "SIE file content, base64" },
- { "path", ARG_STR, 0, NULL, NULL,
- "Server-side path; alternative to content_base64" },
-};
-
-static const struct cmd_arg args_customer_list[] = {
- { "active_only", ARG_BOOL, 0, NULL, NULL, "Only active customers" },
-};
-
-static const struct cmd_arg args_customer_get[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Customer id" },
-};
-
-static const struct cmd_arg args_customer_create[] = {
- { "name", ARG_STR, 1, NULL, NULL, "Customer name, unique per org" },
- { "address", ARG_STR, 0, NULL, NULL, "Street address; may contain newlines" },
- { "postal_code", ARG_STR, 0, NULL, NULL, "Postal code" },
- { "city", ARG_STR, 0, NULL, NULL, "City" },
- { "country", ARG_STR, 0, "SE", NULL, "Country code" },
- { "vat_nr", ARG_STR, 0, NULL, NULL, "VAT number" },
- { "email", ARG_STR, 0, NULL, NULL, "E-mail address" },
- { "your_ref", ARG_STR, 0, NULL, NULL, "Customer reference" },
- { "notes", ARG_STR, 0, NULL, NULL, "Free-text notes" },
- { "payment_days", ARG_INT, 0, "30", NULL, "Payment terms in days" },
-};
-
-static const struct cmd_arg args_customer_update[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Customer id" },
- { "name", ARG_STR, 0, NULL, NULL, "Customer name, unique per org" },
- { "address", ARG_STR, 0, NULL, NULL, "Street address" },
- { "postal_code", ARG_STR, 0, NULL, NULL, "Postal code" },
- { "city", ARG_STR, 0, NULL, NULL, "City" },
- { "country", ARG_STR, 0, NULL, NULL, "Country code" },
- { "vat_nr", ARG_STR, 0, NULL, NULL, "VAT number" },
- { "email", ARG_STR, 0, NULL, NULL, "E-mail address" },
- { "your_ref", ARG_STR, 0, NULL, NULL, "Customer reference" },
- { "notes", ARG_STR, 0, NULL, NULL, "Free-text notes" },
- { "payment_days", ARG_INT, 0, NULL, NULL, "Payment terms in days" },
- { "active", ARG_BOOL, 0, NULL, NULL, "Active flag" },
-};
-
-static const struct cmd_arg args_customer_archive[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Customer id" },
- { "active", ARG_BOOL, 1, NULL, NULL, "false archives, true reactivates" },
-};
-
-static const struct cmd_arg args_invoice_sequence_set[] = {
- { "next_number", ARG_INT, 1, NULL, NULL, "Next invoice number" },
-};
-
-static const struct cmd_arg args_invoice_draft[] = {
- { "customer_id", ARG_INT, 1, NULL, NULL, "Customer id" },
- { "invoice_date", ARG_DATE, 1, NULL, NULL, "Invoice date (YYYY-MM-DD)" },
- { "due_date", ARG_DATE, 1, NULL, NULL, "Due date (YYYY-MM-DD)" },
- { "delivery_date", ARG_STR, 0, NULL, NULL, "Delivery date or empty" },
- { "your_ref", ARG_STR, 0, NULL, NULL, "Customer reference" },
- { "our_ref", ARG_STR, 0, NULL, NULL, "Our reference" },
- { "notes", ARG_STR, 0, NULL, NULL, "Free-text notes" },
- { "rows", ARG_JSON, 1, NULL, NULL,
- "Array of {article_no,description,quantity,unit,unit_price_ore,note,"
- "vat_code,account}" },
-};
-
-static const struct cmd_arg args_invoice_get[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Invoice id" },
-};
-
-static const struct cmd_arg args_invoice_list[] = {
- { "customer_id", ARG_INT, 0, NULL, NULL, "Customer filter" },
- { "status", ARG_ENUM, 0, NULL, "issued,credited", "Status filter" },
- { "limit", ARG_INT, 0, "200", NULL, "Page size, 1-1000" },
-};
-
-static const struct cmd_arg args_invoice_send[] = {
- { "id", ARG_INT, 1, NULL, NULL, "Invoice id" },
- { "to", ARG_STR, 0, NULL, NULL,
- "Recipient e-mail; defaults to the customer's address" },
-};
-
-const struct command g_commands[] = {
- { "health", "Liveness probe", PERM_PUBLIC, 0, 0, 0, h_health, NULL, 0 },
- { "meta", "Server metadata and limits", PERM_PUBLIC, 0, 0, 0, h_meta, NULL,
- 0 },
- { "session.open", "Open a session (password or token)", PERM_PUBLIC, 0, 0,
- 0, h_session_open, CMD_ARGS(args_session_open) },
- { "session.close", "Close the current session", PERM_READ, 0, 0, 0,
- h_session_close, NULL, 0 },
- { "session.whoami", "Current user, org, role and scopes", PERM_READ, 0, 0,
- 0, h_session_whoami, NULL, 0 },
- { "session.list_orgs", "Orgs the current user is a member of", PERM_READ,
- 0, 0, 0, h_session_list_orgs, NULL, 0 },
- { "session.use_org", "Switch active org", PERM_READ, 0, 0, 0,
- h_session_use_org, CMD_ARGS(args_session_use_org) },
- { "org.create", "Create an org and become its owner", PERM_READ, 0, 1, 1,
- h_org_create, CMD_ARGS(args_org_create) },
- { "org.list", "List orgs for the current user", PERM_READ, 0, 0, 0,
- h_org_list, NULL, 0 },
- { "org.get", "Get org details", PERM_READ, 1, 0, 0, h_org_get, NULL, 0 },
- { "org.update", "Update org details (owner)", PERM_OWNER, 1, 1, 1,
- h_org_update, CMD_ARGS(args_org_update) },
- { "org.member_list", "List org members", PERM_READ, 1, 0, 0,
- h_org_member_list, NULL, 0 },
- { "org.member_add", "Add a member to the org", PERM_OWNER, 1, 1, 1,
- h_org_member_add, CMD_ARGS(args_member_role) },
- { "org.member_set_role", "Change a member's role", PERM_OWNER, 1, 1, 1,
- h_org_member_set_role, CMD_ARGS(args_member_role) },
- { "org.member_remove", "Remove a member from the org", PERM_OWNER, 1, 1, 1,
- h_org_member_remove, CMD_ARGS(args_member_username) },
- { "board.list", "List board members", PERM_READ, 1, 0, 0, h_board_list,
- NULL, 0 },
- { "board.add", "Add a board member", PERM_OWNER, 1, 1, 1, h_board_add,
- CMD_ARGS(args_board_add) },
- { "board.update", "Update a board member", PERM_OWNER, 1, 1, 1,
- h_board_update, CMD_ARGS(args_board_update) },
- { "board.remove", "Remove a board member", PERM_OWNER, 1, 1, 1,
- h_board_remove, CMD_ARGS(args_board_remove) },
- { "user.create", "Create a user (system admin)", PERM_ADMIN, 0, 1, 1,
- h_user_create, CMD_ARGS(args_user_create) },
- { "user.list", "List users (system admin)", PERM_ADMIN, 0, 0, 0,
- h_user_list, NULL, 0 },
- { "token.create", "Create an API token for the active org", PERM_READ, 1,
- 1, 1, h_token_create, CMD_ARGS(args_token_create) },
- { "token.list", "List your API tokens in the active org", PERM_READ, 1, 0,
- 0, h_token_list, NULL, 0 },
- { "token.revoke", "Revoke an API token", PERM_READ, 1, 1, 1,
- h_token_revoke, CMD_ARGS(args_token_revoke) },
+const struct command g_cmd_discovery[] = {
{ "describe", "List implemented commands and permissions", PERM_READ, 0, 0,
0, h_describe, CMD_ARGS(args_describe) },
{ "agent.instructions", "Workflow rules for agents", PERM_READ, 0, 0, 0,
h_agent_instructions, NULL, 0 },
- { "audit.list", "Read the audit log for the active org", PERM_READ, 1, 0,
- 0, h_audit_list, CMD_ARGS(args_audit_list) },
- { "audit.verify", "Verify the voucher and audit hash chains", PERM_READ, 0,
- 0, 0, h_audit_verify, CMD_ARGS(args_audit_verify) },
- { "backup.snapshot", "Create a consistent database snapshot", PERM_ADMIN,
- 0, 1, 0, h_backup_snapshot, CMD_ARGS(args_backup_snapshot) },
- { "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) },
- { "fiscal_year.list", "List fiscal years", PERM_READ, 1, 0, 0,
- h_fiscal_year_list, NULL, 0 },
- { "fiscal_year.get", "Get a fiscal year", PERM_READ, 1, 0, 0,
- h_fiscal_year_get, CMD_ARGS(args_fiscal_year_get) },
- { "fiscal_year.open", "Open a new fiscal year", PERM_OWNER, 1, 1, 1,
- h_fiscal_year_open, CMD_ARGS(args_fiscal_year_open) },
- { "fiscal_year.close", "Close a fiscal year", PERM_OWNER, 1, 1, 0,
- h_fiscal_year_close, CMD_ARGS(args_fiscal_year_close) },
- { "fiscal_year.reopen", "Reopen a closed fiscal year", PERM_OWNER, 1, 1, 1,
- h_fiscal_year_reopen, CMD_ARGS(args_fiscal_year_reopen) },
- { "fiscal_year.update", "Update fiscal-year metadata (dividend)",
- PERM_WRITE, 1, 1, 1, h_fiscal_year_update,
- CMD_ARGS(args_fiscal_year_update) },
- { "period.lock", "Lock a period through a date", PERM_OWNER, 1, 1, 1,
- h_period_lock, CMD_ARGS(args_period_lock) },
- { "period.unlock", "Remove a period lock", PERM_OWNER, 1, 1, 1,
- h_period_unlock, CMD_ARGS(args_period_unlock) },
- { "voucher.post", "Post an immutable voucher (rows or template+x)",
- PERM_WRITE, 1, 1, 1, h_voucher_post, CMD_ARGS(args_voucher_post) },
- { "voucher.get", "Get a voucher with rows", PERM_READ, 1, 0, 0,
- h_voucher_get, CMD_ARGS(args_voucher_get) },
- { "voucher.list", "List vouchers", PERM_READ, 1, 0, 0, h_voucher_list,
- CMD_ARGS(args_voucher_list) },
- { "voucher.correct", "Post an ändringsverifikat", PERM_WRITE, 1, 1, 1,
- h_voucher_correct, CMD_ARGS(args_voucher_correct) },
- { "bokslut.post", "Year-end: dispositions, tax and result transfer",
- PERM_WRITE, 1, 1, 1, h_bokslut_post, CMD_ARGS(args_bokslut_post) },
- { "settings.get", "Read org settings", PERM_READ, 1, 0, 0,
- h_settings_get, NULL, 0 },
- { "settings.set", "Change an org setting", PERM_WRITE, 1, 1, 1,
- h_settings_set, CMD_ARGS(args_settings_set) },
- { "bank.import", "Import a bank statement (SEB CSV)", PERM_WRITE, 1, 1, 1,
- h_bank_import, CMD_ARGS(args_bank_import) },
- { "bank.list", "List bank transactions, matches and suggestions",
- PERM_READ, 1, 0, 0, h_bank_list, CMD_ARGS(args_bank_list) },
- { "bank.match", "Match a bank transaction to a voucher", PERM_WRITE, 1, 1,
- 1, h_bank_match, CMD_ARGS(args_bank_match) },
- { "bank.unmatch", "Remove a transaction/voucher match", PERM_WRITE, 1, 1,
- 1, h_bank_unmatch, CMD_ARGS(args_bank_match) },
- { "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) },
- { "template.list", "List voucher templates", PERM_READ, 1, 0, 0,
- h_template_list, CMD_ARGS(args_template_list) },
- { "template.get", "Get a voucher template with its rows", PERM_READ, 1, 0,
- 0, h_template_get, CMD_ARGS(args_template_get) },
- { "template.create", "Create a voucher template", PERM_WRITE, 1, 1, 1,
- h_template_create, CMD_ARGS(args_template_create) },
- { "template.update", "Update a voucher template", PERM_WRITE, 1, 1, 1,
- h_template_update, CMD_ARGS(args_template_update) },
- { "template.archive", "Archive a voucher template", PERM_WRITE, 1, 1, 1,
- h_template_archive, CMD_ARGS(args_template_archive) },
- { "attachment.put", "Store an underlag (receipt, invoice)", PERM_WRITE, 1,
- 1, 1, h_attachment_put, CMD_ARGS(args_attachment_put) },
- { "attachment.link", "Link an existing attachment to a voucher",
- PERM_WRITE, 1, 1, 1, h_attachment_link, CMD_ARGS(args_attachment_link) },
- { "attachment.unlink", "Remove an attachment link", PERM_WRITE, 1, 1, 1,
- h_attachment_unlink, CMD_ARGS(args_attachment_unlink) },
- { "attachment.get", "Fetch an attachment", PERM_READ, 1, 0, 0,
- h_attachment_get, CMD_ARGS(args_attachment_get) },
- { "attachment.list", "List attachments / inbox", PERM_READ, 1, 0, 0,
- h_attachment_list, CMD_ARGS(args_attachment_list) },
- { "report.trial_balance", "Saldobalans (IB, period, UB)", PERM_READ, 1, 0,
- 0, h_report_trial_balance, CMD_ARGS(args_report_trial_balance) },
- { "report.income_statement", "Resultaträkning", PERM_READ, 1, 0, 0,
- h_report_income_statement, CMD_ARGS(args_report_income_statement) },
- { "report.balance_sheet", "Balansräkning", PERM_READ, 1, 0, 0,
- h_report_balance_sheet, CMD_ARGS(args_report_balance_sheet) },
- { "report.vat", "Momsdeklaration ruta för ruta", PERM_READ, 1, 0, 0,
- h_report_vat, CMD_ARGS(args_report_vat) },
- { "report.general_ledger", "Huvudbok", PERM_READ, 1, 0, 0,
- h_report_general_ledger, CMD_ARGS(args_report_general_ledger) },
- { "report.voucher_list", "Verifikationslista", PERM_READ, 1, 0, 0,
- h_report_voucher_list, CMD_ARGS(args_report_voucher_list) },
- { "report.vat_eskd", "eSKD XML for the momsdeklaration (ISO-8859-1)",
- PERM_READ, 1, 0, 0, h_report_vat_eskd, CMD_ARGS(args_report_vat_eskd) },
- { "sru.export", "INK2 SRU files (INFO.SRU + BLANKETTER.SRU)",
- PERM_READ, 1, 0, 0, h_sru_export, CMD_ARGS(args_sru_export) },
- { "sie.export", "Export SIE 4 (PC8)", PERM_READ, 1, 0, 0, h_sie_export,
- CMD_ARGS(args_sie_export) },
- { "sie.import", "Import SIE 4 into an empty fiscal year", PERM_WRITE, 1, 1,
- 1, h_sie_import, CMD_ARGS(args_sie_import) },
- { "customer.list", "List customers ordered by name", PERM_READ, 1, 0, 0,
- h_customer_list, CMD_ARGS(args_customer_list) },
- { "customer.get", "Get one customer", PERM_READ, 1, 0, 0, h_customer_get,
- CMD_ARGS(args_customer_get) },
- { "customer.create", "Create a customer", PERM_WRITE, 1, 1, 1,
- h_customer_create, CMD_ARGS(args_customer_create) },
- { "customer.update", "Update a customer (merged)", PERM_WRITE, 1, 1, 1,
- h_customer_update, CMD_ARGS(args_customer_update) },
- { "customer.archive", "Archive or reactivate a customer", PERM_WRITE, 1, 1,
- 1, h_customer_archive, CMD_ARGS(args_customer_archive) },
- { "invoice.sequence_get", "Read the next invoice number", PERM_READ, 1, 0,
- 0, h_invoice_sequence_get, NULL, 0 },
- { "invoice.sequence_set", "Set the next invoice number (owner)",
- PERM_OWNER, 1, 1, 1, h_invoice_sequence_set,
- CMD_ARGS(args_invoice_sequence_set) },
- { "invoice.preview", "Render an invoice draft without storing it",
- PERM_READ, 1, 0, 0, h_invoice_preview, CMD_ARGS(args_invoice_draft) },
- { "invoice.issue", "Issue an invoice: number, PDF and voucher",
- PERM_WRITE, 1, 1, 1, h_invoice_issue, CMD_ARGS(args_invoice_draft) },
- { "invoice.get", "Get an invoice with rows", PERM_READ, 1, 0, 0,
- h_invoice_get, CMD_ARGS(args_invoice_get) },
- { "invoice.list", "List invoices, newest first", PERM_READ, 1, 0, 0,
- h_invoice_list, CMD_ARGS(args_invoice_list) },
- { "invoice.pdf", "Fetch the stored invoice PDF", PERM_READ, 1, 0, 0,
- h_invoice_pdf, CMD_ARGS(args_invoice_get) },
- { "invoice.send", "E-mail the stored invoice PDF to the customer",
- PERM_WRITE, 1, 1, 1, h_invoice_send, CMD_ARGS(args_invoice_send) },
};
-const size_t g_commands_count = sizeof g_commands / sizeof g_commands[0];
+const struct cmd_table g_cmd_table_discovery = {
+ g_cmd_discovery, sizeof g_cmd_discovery / sizeof g_cmd_discovery[0]
+};
const struct command *command_find(const char *name)
{
if (!name)
return NULL;
- for (size_t i = 0; i < g_commands_count; i++)
- if (strcmp(g_commands[i].name, name) == 0)
- return &g_commands[i];
+ for (size_t t = 0; t < g_command_tables_count; t++) {
+ const struct cmd_table *tab = g_command_tables[t];
+ for (size_t i = 0; i < tab->n; i++)
+ if (strcmp(tab->cmds[i].name, name) == 0)
+ return &tab->cmds[i];
+ }
return NULL;
}
@@ -8468,3 +191,35 @@ const char *perm_scope(enum cmd_perm p)
}
return NULL;
}
+
+extern const struct cmd_table g_cmd_table_auth;
+extern const struct cmd_table g_cmd_table_org;
+extern const struct cmd_table g_cmd_table_users;
+extern const struct cmd_table g_cmd_table_audit;
+extern const struct cmd_table g_cmd_table_accounts;
+extern const struct cmd_table g_cmd_table_fiscal;
+extern const struct cmd_table g_cmd_table_vouchers;
+extern const struct cmd_table g_cmd_table_templates;
+extern const struct cmd_table g_cmd_table_settings;
+extern const struct cmd_table g_cmd_table_customers;
+extern const struct cmd_table g_cmd_table_invoices;
+extern const struct cmd_table g_cmd_table_rules;
+extern const struct cmd_table g_cmd_table_attachments;
+extern const struct cmd_table g_cmd_table_reports;
+extern const struct cmd_table g_cmd_table_sru;
+extern const struct cmd_table g_cmd_table_sie;
+extern const struct cmd_table g_cmd_table_bokslut;
+extern const struct cmd_table g_cmd_table_bank;
+
+const struct cmd_table *const g_command_tables[] = {
+ &g_cmd_table_auth, &g_cmd_table_org, &g_cmd_table_users,
+ &g_cmd_table_discovery, &g_cmd_table_audit, &g_cmd_table_accounts,
+ &g_cmd_table_fiscal, &g_cmd_table_vouchers, &g_cmd_table_bokslut,
+ &g_cmd_table_settings, &g_cmd_table_bank, &g_cmd_table_rules,
+ &g_cmd_table_templates, &g_cmd_table_attachments, &g_cmd_table_reports,
+ &g_cmd_table_sru, &g_cmd_table_sie, &g_cmd_table_customers,
+ &g_cmd_table_invoices,
+};
+
+const size_t g_command_tables_count =
+ sizeof g_command_tables / sizeof g_command_tables[0];