#include #include #include #include #include #include #include #include #include #include #include #include #include "client.h" #include "config.h" #include "db.h" #include "protocol.h" #include "sessions.h" #include "util.h" #include "yyjson.h" static sqlite3 *g_db; static char g_session[128]; static int g_failures; #define CHECK(cond) \ do { \ if (!(cond)) { \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \ #cond); \ g_failures++; \ } \ } while (0) static char *reqf(const char *fmt, ...) __attribute__((format(printf, 1, 2))); static char *reqf(const char *fmt, ...) { static char buf[4096]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof buf, fmt, ap); va_end(ap); return buf; } static yyjson_doc *call(const char *req) { char *resp = protocol_handle_line(g_db, req, strlen(req)); if (!resp) { CHECK(0 && "null response"); return NULL; } yyjson_doc *d = yyjson_read(resp, strlen(resp), 0); free(resp); return d; } static yyjson_val *jget(yyjson_doc *d, const char *path) { if (!d) return NULL; char tmp[128]; snprintf(tmp, sizeof tmp, "%s", path); yyjson_val *v = yyjson_doc_get_root(d); char *save = NULL; for (char *tok = strtok_r(tmp, ".", &save); tok; tok = strtok_r(NULL, ".", &save)) { if (!v) return NULL; if (yyjson_is_obj(v)) { v = yyjson_obj_get(v, tok); } else if (yyjson_is_arr(v)) { char *end = NULL; long idx = strtol(tok, &end, 10); if (!end || *end) return NULL; v = yyjson_arr_get(v, (size_t)idx); } else { return NULL; } } return v; } static const char *jstr(yyjson_doc *d, const char *path) { yyjson_val *v = jget(d, path); return v && yyjson_is_str(v) ? yyjson_get_str(v) : NULL; } static int64_t jint(yyjson_doc *d, const char *path) { yyjson_val *v = jget(d, path); return v && yyjson_is_int(v) ? yyjson_get_int(v) : -1; } static int jbool(yyjson_doc *d, const char *path) { yyjson_val *v = jget(d, path); return v && yyjson_is_bool(v) ? yyjson_get_bool(v) : 0; } static void check_str(yyjson_doc *d, const char *path, const char *expect, int line) { const char *got = jstr(d, path); if (!got || strcmp(got, expect) != 0) { fprintf(stderr, "FAIL %s:%d: %s = %s, expected %s\n", __FILE__, line, path, got ? got : "(null)", expect); g_failures++; } } #define CHECK_STR(doc, path, expect) check_str(doc, path, expect, __LINE__) static void check_ok(yyjson_doc *d, int line) { if (!jbool(d, "ok")) { const char *code = jstr(d, "error.code"); fprintf(stderr, "FAIL %s:%d: not ok (%s)\n", __FILE__, line, code ? code : "no code"); g_failures++; } } #define CHECK_OK(doc) check_ok(doc, __LINE__) static int64_t find_amount(yyjson_doc *d, const char *array_path, const char *key, const char *value, const char *field) { yyjson_val *arr = jget(d, array_path); size_t n = arr && yyjson_is_arr(arr) ? yyjson_arr_size(arr) : 0; for (size_t i = 0; i < n; i++) { yyjson_val *row = yyjson_arr_get(arr, i); yyjson_val *kv = yyjson_obj_get(row, key); if (kv && yyjson_is_str(kv) && strcmp(yyjson_get_str(kv), value) == 0) { yyjson_val *v = yyjson_obj_get(row, field); return v && yyjson_is_int(v) ? yyjson_get_int(v) : -2; } } return -1; } static int64_t vat_box(yyjson_doc *d, const char *box) { return find_amount(d, "result.boxes", "box", box, "amount_ore"); } static yyjson_doc *call_sie_import(const char *session, int64_t org, const char *b64, int dry) { size_t len = strlen(b64) + 1024; char *req = xmalloc(len); snprintf(req, len, "{\"v\":1,\"id\":\"imp\",\"cmd\":\"sie.import\",\"session\":\"%s\"," "\"org\":%lld%s,\"args\":{\"content_base64\":\"%s\"}}", session, (long long)org, dry ? ",\"dry_run\":true" : "", b64); yyjson_doc *d = call(req); free(req); return d; } static int login(const char *user, const char *pass) { yyjson_doc *d = call(reqf( "{\"v\":1,\"id\":\"l\",\"cmd\":\"session.open\",\"args\":" "{\"method\":\"password\",\"username\":\"%s\",\"password\":\"%s\"}}", user, pass)); int ok = jbool(d, "ok"); if (ok) snprintf(g_session, sizeof g_session, "%s", jstr(d, "result.session")); yyjson_doc_free(d); return ok; } static void echo_child(int wfd, int tls) { int sfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); if (sfd < 0) _exit(1); struct sockaddr_in sa; memset(&sa, 0, sizeof sa); sa.sin_family = AF_INET; sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); sa.sin_port = 0; if (bind(sfd, (struct sockaddr *)&sa, sizeof sa) != 0 || listen(sfd, 1) != 0) _exit(1); socklen_t slen = sizeof sa; if (getsockname(sfd, (struct sockaddr *)&sa, &slen) != 0) _exit(1); int port = ntohs(sa.sin_port); if (write(wfd, &port, sizeof port) != (ssize_t)sizeof port) _exit(1); close(wfd); int cfd = accept(sfd, NULL, NULL); if (cfd < 0) _exit(1); char buf[256]; if (tls) { SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); if (!ctx || SSL_CTX_use_certificate_chain_file(ctx, "tests/tls_test_cert.pem") != 1 || SSL_CTX_use_PrivateKey_file(ctx, "tests/tls_test_key.pem", SSL_FILETYPE_PEM) != 1) _exit(1); SSL *ssl = SSL_new(ctx); SSL_set_fd(ssl, cfd); if (SSL_accept(ssl) != 1) _exit(1); size_t got = 0; while (got < sizeof buf) { int n = SSL_read(ssl, buf + got, (int)(sizeof buf - got)); if (n <= 0) break; got += (size_t)n; if (memchr(buf, '\n', got)) break; } (void)SSL_write(ssl, "tls-ok", 6); SSL_shutdown(ssl); SSL_free(ssl); SSL_CTX_free(ctx); } else { size_t got = 0; while (got < sizeof buf) { ssize_t n = read(cfd, buf + got, sizeof buf - got); if (n <= 0) break; got += (size_t)n; if (memchr(buf, '\n', got)) break; } (void)write(cfd, "tcp-ok", 6); } close(cfd); close(sfd); _exit(0); } /* variant 0: plaintext tcp; 1: TLS trusting the test CA; 2: TLS without the CA, which must fail certificate verification. */ static void test_transport(void) { signal(SIGPIPE, SIG_IGN); for (int variant = 0; variant < 3; variant++) { int tls = variant > 0; int pfd[2]; if (pipe(pfd) != 0) { CHECK(0 && "pipe"); return; } pid_t pid = fork(); if (pid == 0) { close(pfd[0]); echo_child(pfd[1], tls); } close(pfd[1]); int port = 0; if (read(pfd[0], &port, sizeof port) != (ssize_t)sizeof port) port = 0; close(pfd[0]); if (port <= 0) { CHECK(0 && "no test server port"); waitpid(pid, NULL, 0); continue; } if (variant == 1) setenv("BOKFD_TLS_CA", "tests/tls_test_cert.pem", 1); else unsetenv("BOKFD_TLS_CA"); char target[64]; snprintf(target, sizeof target, "%s:localhost:%d", tls ? "tls" : "tcp", port); struct client_conn c; if (variant == 2) { CHECK(client_connect(target, &c) != 0); waitpid(pid, NULL, 0); continue; } CHECK(client_connect(target, &c) == 0); CHECK(client_send_line(&c, "{\"v\":1}") == 0); char *line = client_read_line(&c); CHECK(line && strncmp(line, tls ? "tls-ok" : "tcp-ok", 6) == 0); if (!line) fprintf(stderr, "transport error: %s\n", client_last_error()); free(line); client_close(&c); waitpid(pid, NULL, 0); } } int main(void) { char tmpdir[] = "/tmp/bokf-test-XXXXXX"; if (!mkdtemp(tmpdir)) { perror("mkdtemp"); return 1; } char dbpath[512], backupdir[512]; snprintf(dbpath, sizeof dbpath, "%s/bokfd.db", tmpdir); snprintf(backupdir, sizeof backupdir, "%s/backup", tmpdir); mkdir(backupdir, 0700); config_defaults(); g_cfg.log_level = 0; free(g_cfg.backup_dir); g_cfg.backup_dir = xstrdup(backupdir); free(g_cfg.export_dir); g_cfg.export_dir = xstrdup(tmpdir); free(g_cfg.chart_k2_file); g_cfg.chart_k2_file = xstrdup("data/bas_k2.csv"); free(g_cfg.chart_k3_file); g_cfg.chart_k3_file = xstrdup("data/bas_k3.csv"); char *err = NULL; if (db_open(dbpath, &g_db, &err) != 0) { fprintf(stderr, "db_open: %s\n", err ? err : "?"); return 1; } int64_t admin_id = 0; if (db_create_user(g_db, "admin", "Admin", "secret123", 1, &admin_id, &err) != 0) { fprintf(stderr, "create admin: %s\n", err ? err : "?"); return 1; } sessions_init(3600); yyjson_doc *d; d = call("{\"v\":1,\"id\":\"1\",\"cmd\":\"health\"}"); CHECK_OK(d); CHECK_STR(d, "result.status", "ok"); yyjson_doc_free(d); d = call("{\"v\":1,\"id\":\"2\",\"cmd\":\"meta\"}"); CHECK_OK(d); CHECK(jint(d, "result.protocol") == 1); CHECK_STR(d, "result.server", "bokfd"); yyjson_doc_free(d); d = call("{\"v\":1,\"id\":\"3\",\"cmd\":\"nope\"}"); CHECK(!jbool(d, "ok")); CHECK_STR(d, "error.code", "UNKNOWN_COMMAND"); yyjson_doc_free(d); d = call("{\"v\":9,\"id\":\"4\",\"cmd\":\"health\"}"); CHECK_STR(d, "error.code", "UNSUPPORTED_VERSION"); yyjson_doc_free(d); d = call("not json"); CHECK_STR(d, "error.code", "PARSE_ERROR"); yyjson_doc_free(d); d = call("{\"v\":1,\"id\":\"5\",\"cmd\":\"describe\"}"); CHECK_STR(d, "error.code", "AUTH_REQUIRED"); yyjson_doc_free(d); CHECK(!login("admin", "wrong")); CHECK(!login("nobody", "wrong")); CHECK(login("admin", "secret123")); d = call(reqf("{\"v\":1,\"id\":\"6\",\"cmd\":\"session.whoami\"," "\"session\":\"%s\"}", g_session)); CHECK_OK(d); CHECK_STR(d, "result.user.username", "admin"); CHECK(jbool(d, "result.user.is_admin")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"7\",\"cmd\":\"org.create\"," "\"session\":\"%s\",\"args\":" "{\"name\":\"AB Ett\",\"org_nr\":\"5560123456\"}}", g_session)); CHECK_OK(d); int64_t org_id = jint(d, "result.id"); CHECK(org_id > 0); CHECK_STR(d, "result.name", "AB Ett"); CHECK_STR(d, "result.org_nr", "5560123456"); CHECK(jint(d, "result.fiscal_year_id") > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8\",\"cmd\":\"org.list\"," "\"session\":\"%s\"}", g_session)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); CHECK_STR(d, "result.items.0.role", "owner"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8b\",\"cmd\":\"org.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"org_nr\":\"559331-2126\",\"vat_nr\":\"SE559331212601\"," "\"address\":\"Lingonvägen 9\",\"postal_code\":\"192 48\"," "\"city\":\"Sollentuna\",\"email\":\"a@b.se\"," "\"phone\":\"+46700000000\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.vat_nr", "SE559331212601"); CHECK_STR(d, "result.org_nr", "559331-2126"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8c\",\"cmd\":\"org.get\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.address", "Lingonvägen 9"); CHECK_STR(d, "result.postal_code", "192 48"); CHECK_STR(d, "result.city", "Sollentuna"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8d\",\"cmd\":\"org.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"moms_period\":\"week\"}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8e\",\"cmd\":\"org.update\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" "{\"city\":\"Nyköping\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"8f\",\"cmd\":\"org.get\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.city", "Sollentuna"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"9\",\"cmd\":\"session.use_org\"," "\"session\":\"%s\",\"args\":{\"org\":%d}}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.role", "owner"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"10\",\"cmd\":\"org.get\"," "\"session\":\"%s\",\"org\":10}", g_session)); CHECK_STR(d, "error.code", "ORG_FORBIDDEN"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"11\",\"cmd\":\"org.get\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.moms_period", "month"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"12\",\"cmd\":\"token.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"label\":\"test\",\"scopes\":[\"read\",\"write\"]}}", g_session, (int)org_id)); CHECK_OK(d); char token_copy[128] = ""; snprintf(token_copy, sizeof token_copy, "%s", jstr(d, "result.token")); CHECK(strncmp(token_copy, "bokf_", 5) == 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"13\",\"cmd\":\"token.list\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"14\",\"cmd\":\"token.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"label\":\"ro\",\"scopes\":[\"read\"]}}", g_session, (int)org_id)); CHECK_OK(d); char ro_token[128] = ""; snprintf(ro_token, sizeof ro_token, "%s", jstr(d, "result.token")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"15\",\"cmd\":\"session.open\",\"args\":" "{\"method\":\"token\",\"token\":\"%s\"}}", token_copy)); CHECK_OK(d); CHECK(jint(d, "result.active_org") == org_id); snprintf(g_session, sizeof g_session, "%s", jstr(d, "result.session")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"16\",\"cmd\":\"session.whoami\"," "\"session\":\"%s\"}", g_session)); CHECK_STR(d, "result.scopes", "read,write"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"17\",\"cmd\":\"session.open\",\"args\":" "{\"method\":\"token\",\"token\":\"%s\"}}", ro_token)); CHECK_OK(d); char ro_session[128] = ""; snprintf(ro_session, sizeof ro_session, "%s", jstr(d, "result.session")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"18\",\"cmd\":\"org.member_add\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"bob\",\"role\":\"viewer\"}}", ro_session, (int)org_id)); CHECK_STR(d, "error.code", "FORBIDDEN"); yyjson_doc_free(d); CHECK(login("admin", "secret123")); d = call(reqf("{\"v\":1,\"id\":\"19\",\"cmd\":\"user.create\"," "\"session\":\"%s\",\"args\":" "{\"username\":\"bob\",\"password\":\"hunter22\"," "\"display_name\":\"Bob\"}}", g_session)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"20\",\"cmd\":\"user.create\"," "\"session\":\"%s\",\"args\":" "{\"username\":\"bob\",\"password\":\"hunter22\"}}", g_session)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"21\",\"cmd\":\"org.member_add\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"bob\",\"role\":\"viewer\"}}", g_session, (int)org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"22\",\"cmd\":\"org.member_list\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK(yyjson_arr_size(jget(d, "result.items")) == 2); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"23\",\"cmd\":\"org.member_set_role\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"bob\",\"role\":\"bookkeeper\"}}", g_session, (int)org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"24\",\"cmd\":\"org.member_remove\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"admin\"}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"25\",\"cmd\":\"org.member_remove\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"username\":\"bob\"}}", g_session, (int)org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"26\",\"cmd\":\"audit.verify\"," "\"session\":\"%s\"}", g_session)); CHECK_OK(d); CHECK(jbool(d, "result.ok")); CHECK(jint(d, "result.checked") > 5); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"27\",\"cmd\":\"audit.list\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) > 0); yyjson_doc_free(d); err = NULL; CHECK(db_exec(g_db, "UPDATE audit_log SET action='x'", &err) != 0); free(err); err = NULL; CHECK(db_exec(g_db, "DELETE FROM audit_log", &err) != 0); free(err); d = call(reqf("{\"v\":1,\"id\":\"27\",\"cmd\":\"backup.snapshot\"," "\"session\":\"%s\"}", ro_session)); CHECK_STR(d, "error.code", "FORBIDDEN"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"28\",\"cmd\":\"backup.snapshot\"," "\"session\":\"%s\"}", g_session)); CHECK_OK(d); CHECK(jint(d, "result.size") > 0); CHECK(jstr(d, "result.sha256") && strlen(jstr(d, "result.sha256")) == 64); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"28b\",\"cmd\":\"token.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"label\":\"backup\",\"scopes\":[\"admin\"]}}", g_session, (int)org_id)); CHECK_OK(d); char backup_token[128] = ""; snprintf(backup_token, sizeof backup_token, "%s", jstr(d, "result.token")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"28c\",\"cmd\":\"session.open\",\"args\":" "{\"method\":\"token\",\"token\":\"%s\"}}", backup_token)); CHECK_OK(d); char backup_session[128] = ""; snprintf(backup_session, sizeof backup_session, "%s", jstr(d, "result.session")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"28d\",\"cmd\":\"backup.snapshot\"," "\"session\":\"%s\",\"args\":{\"dest\":\"%s/snap-token.db\"}}", backup_session, tmpdir)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"29\",\"cmd\":\"describe\"," "\"session\":\"%s\"}", g_session)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.commands")) > 10); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"30\",\"cmd\":\"describe\"," "\"session\":\"%s\",\"args\":{\"cmd\":\"session.open\"}}", g_session)); CHECK_STR(d, "result.command.name", "session.open"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"31\",\"cmd\":\"agent.instructions\"," "\"session\":\"%s\"}", g_session)); CHECK_OK(d); CHECK(jstr(d, "result.content") && strstr(jstr(d, "result.content"), "dry_run") != NULL); yyjson_doc_free(d); /* ---------------- M2: kontoplan, vouchers, reports, SIE ---------- */ d = call(reqf("{\"v\":1,\"id\":\"40\",\"cmd\":\"account.list\"," "\"session\":\"%s\",\"org\":%d," "\"args\":{\"active_only\":true}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) > 1000); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"41\",\"cmd\":\"account.get\"," "\"session\":\"%s\",\"org\":%d," "\"args\":{\"number\":\"1930\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.type", "asset"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"42\",\"cmd\":\"account.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"number\":\"0099\",\"name\":\"Testkonto\"," "\"type\":\"expense\"}}", g_session, (int)org_id)); CHECK_OK(d); int64_t acc2999 = jint(d, "result.id"); CHECK(acc2999 > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"43\",\"cmd\":\"account.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"number\":\"0099\",\"name\":\"Dup\"," "\"type\":\"expense\"}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"44\",\"cmd\":\"account.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"id\":%lld,\"name\":\"Testkonto 2\"}}", g_session, (int)org_id, (long long)acc2999)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"45\",\"cmd\":\"account.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", g_session, (int)org_id, (long long)acc2999)); CHECK_STR(d, "result.name", "Testkonto 2"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"46\",\"cmd\":\"fiscal_year.list\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); int64_t fy1 = jint(d, "result.items.0.id"); CHECK(fy1 > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"46a\",\"cmd\":\"fiscal_year.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"dividend_ore\":123456}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); CHECK(jint(d, "result.dividend_ore") == 123456); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"46b\",\"cmd\":\"fiscal_year.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); CHECK(jint(d, "result.dividend_ore") == 123456); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"46c\",\"cmd\":\"fiscal_year.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"dividend_ore\":-1}}", g_session, (int)org_id, (long long)fy1)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); /* sale: 1250.00 kr incl 25% moms */ d = call(reqf("{\"v\":1,\"id\":\"47\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" "\"2026-01-15\",\"description\":\"Försäljning\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":125000}," "{\"account\":\"3001\",\"credit_ore\":100000}," "{\"account\":\"2611\",\"credit_ore\":25000}]}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(jint(d, "result.number") == 1); int64_t sale_id = jint(d, "result.id"); CHECK(sale_id > 0); CHECK(jstr(d, "result.hash") && strlen(jstr(d, "result.hash")) == 64); CHECK(yyjson_arr_size(jget(d, "result.rows")) == 3); yyjson_doc_free(d); /* dry run must not persist */ d = call(reqf("{\"v\":1,\"id\":\"48\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"date\":\"2026-01-16\",\"description\":" "\"Torrkörning\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"49\",\"cmd\":\"voucher.list\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); yyjson_doc_free(d); /* invalid vouchers */ d = call(reqf("{\"v\":1,\"id\":\"50\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" "\"2026-01-17\",\"description\":\"Obalans\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":90}]}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "UNBALANCED"); CHECK(jint(d, "error.details.difference_ore") == 10); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"51\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" "\"2026-01-17\",\"description\":\"Fel konto\",\"rows\":[" "{\"account\":\"9999\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "ACCOUNT_NOT_FOUND"); yyjson_doc_free(d); /* reports based on the single sale */ d = call(reqf("{\"v\":1,\"id\":\"52\",\"cmd\":\"report.income_statement\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK(jint(d, "result.result_ore") == 100000); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"53\",\"cmd\":\"report.vat\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-01-01\",\"to\":\"2026-01-31\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(vat_box(d, "05") == 100000); CHECK(vat_box(d, "10") == 25000); yyjson_doc_free(d); /* huvudbok: one account block per account with activity or IB */ d = call(reqf("{\"v\":1,\"id\":\"54a\",\"cmd\":\"report.general_ledger\"" ",\"session\":\"%s\",\"org\":%d,\"args\":" "{\"fiscal_year\":%lld}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.accounts")) == 3); CHECK(find_amount(d, "result.accounts", "account", "1930", "debit_ore") == 125000); CHECK(find_amount(d, "result.accounts", "account", "1930", "ub_ore") == 125000); CHECK(find_amount(d, "result.accounts", "account", "3001", "ub_ore") == -100000); CHECK(yyjson_arr_size(jget(d, "result.accounts.0.rows")) == 1); CHECK(jint(d, "result.accounts.0.rows.0.saldo_ore") == 125000); CHECK_STR(d, "result.last_voucher.series", "A"); CHECK(jint(d, "result.last_voucher.number") == 1); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"54b\",\"cmd\":\"report.general_ledger\"" ",\"session\":\"%s\",\"org\":%d,\"args\":" "{\"fiscal_year\":%lld,\"accounts\":[\"1930\"]}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.accounts")) == 1); yyjson_doc_free(d); /* verifikationslista: vouchers with their rows and totals */ d = call(reqf("{\"v\":1,\"id\":\"54c\",\"cmd\":\"report.voucher_list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"fiscal_year\":%lld}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.vouchers")) == 1); CHECK_STR(d, "result.vouchers.0.series", "A"); CHECK(jint(d, "result.vouchers.0.number") == 1); CHECK(yyjson_arr_size(jget(d, "result.vouchers.0.rows")) == 3); CHECK(jint(d, "result.totals.debit_ore") == 125000); CHECK(jint(d, "result.totals.credit_ore") == 125000); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"54d\",\"cmd\":\"report.voucher_list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"fiscal_year\":%lld,\"series\":\"ZZ\"}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.vouchers")) == 0); yyjson_doc_free(d); /* eSKD: ISO-8859-1 XML for the momsdeklaration */ d = call(reqf("{\"v\":1,\"id\":\"54e\",\"cmd\":\"report.vat_eskd\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-01-01\",\"to\":\"2026-01-31\"," "\"upplysning\":\"Blå\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.org_nr", "559331-2126"); CHECK_STR(d, "result.period", "202601"); char *b64 = jstr(d, "result.content_base64"); unsigned char *xmlb = NULL; size_t xmll = 0; CHECK(b64 && util_b64_decode(b64, strlen(b64), &xmlb, &xmll) == 0); char *xml = xmalloc(xmll + 1); memcpy(xml, xmlb, xmll); xml[xmll] = '\0'; free(xmlb); CHECK(strstr(xml, "") != NULL); CHECK(strstr(xml, "559331-2126") != NULL); CHECK(strstr(xml, "202601") != NULL); CHECK(strstr(xml, "1000") != NULL); CHECK(strstr(xml, "250") != NULL); CHECK(strstr(xml, "250") != NULL); CHECK(strstr(xml, "Bl\xe5") != NULL); free(xml); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"54\",\"cmd\":\"report.trial_balance\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK(find_amount(d, "result.accounts", "account", "1930", "ub_ore") == 125000); CHECK(jint(d, "result.totals.debit_ore") == jint(d, "result.totals.credit_ore")); yyjson_doc_free(d); /* idempotency */ d = call(reqf("{\"v\":1,\"id\":\"55\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" "\"2026-01-20\",\"description\":\"Faktura 2\"," "\"client_ref\":\"ref-1\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":50000}," "{\"account\":\"3001\",\"credit_ore\":50000}]}}", g_session, (int)org_id)); CHECK_OK(d); int64_t v2 = jint(d, "result.id"); CHECK(!jbool(d, "result.replayed")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"56\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" "\"2026-01-20\",\"description\":\"Faktura 2\"," "\"client_ref\":\"ref-1\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":50000}," "{\"account\":\"3001\",\"credit_ore\":50000}]}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(jbool(d, "result.replayed")); CHECK(jint(d, "result.id") == v2); yyjson_doc_free(d); /* attachments */ d = call(reqf("{\"v\":1,\"id\":\"57\",\"cmd\":\"attachment.put\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"filename\":" "\"kvitto.txt\",\"mime\":\"text/plain\"," "\"content_base64\":\"aGVsbG8gd29ybGQ=\"}}", g_session, (int)org_id)); CHECK_OK(d); int64_t att_id = jint(d, "result.id"); CHECK(att_id > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"58\",\"cmd\":\"attachment.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"unlinked\":true}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"59\",\"cmd\":\"attachment.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", g_session, (int)org_id, (long long)att_id)); CHECK_OK(d); CHECK_STR(d, "result.content_base64", "aGVsbG8gd29ybGQ="); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"60\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" "\"2026-01-25\",\"description\":\"Lokalhyra\"," "\"attachment_ids\":[%lld],\"rows\":[" "{\"account\":\"5010\",\"debit_ore\":10000}," "{\"account\":\"1930\",\"credit_ore\":10000}]}}", g_session, (int)org_id, (long long)att_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.attachment_ids")) == 1); int64_t rent_id = jint(d, "result.id"); CHECK(rent_id > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61\",\"cmd\":\"attachment.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"unlinked\":true}}", g_session, (int)org_id)); CHECK(yyjson_arr_size(jget(d, "result.items")) == 0); yyjson_doc_free(d); /* the same content may link to several vouchers, but not twice to one */ d = call(reqf("{\"v\":1,\"id\":\"61a\",\"cmd\":\"attachment.put\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"filename\":" "\"kvitto.txt\",\"mime\":\"text/plain\"," "\"content_base64\":\"aGVsbG8gd29ybGQ=\",\"voucher_id\":%lld}}", g_session, (int)org_id, (long long)sale_id)); CHECK_OK(d); CHECK(jint(d, "result.id") == att_id); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61b\",\"cmd\":\"attachment.put\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"filename\":" "\"kvitto.txt\",\"mime\":\"text/plain\"," "\"content_base64\":\"aGVsbG8gd29ybGQ=\",\"voucher_id\":%lld}}", g_session, (int)org_id, (long long)sale_id)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); /* link/unlink an existing attachment */ d = call(reqf("{\"v\":1,\"id\":\"61c\",\"cmd\":\"attachment.put\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"filename\":" "\"extra.txt\",\"content_base64\":\"ZXh0cmE=\"}}", g_session, (int)org_id)); CHECK_OK(d); int64_t orphan = jint(d, "result.id"); CHECK(orphan > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61d\",\"cmd\":\"attachment.link\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"voucher_id\":%lld}}", g_session, (int)org_id, (long long)orphan, (long long)sale_id)); CHECK_OK(d); CHECK(jint(d, "result.voucher_id") == sale_id); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61e\",\"cmd\":\"attachment.link\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"voucher_id\":%lld}}", g_session, (int)org_id, (long long)orphan, (long long)sale_id)); CHECK_STR(d, "error.code", "CONFLICT"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61f\",\"cmd\":\"attachment.unlink\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"voucher_id\":%lld}}", g_session, (int)org_id, (long long)orphan, (long long)sale_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61g\",\"cmd\":\"attachment.unlink\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"voucher_id\":%lld}}", g_session, (int)org_id, (long long)orphan, (long long)sale_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61h\",\"cmd\":\"attachment.link\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"voucher_id\":999999}}", g_session, (int)org_id, (long long)orphan)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); /* listing by voucher returns every linked attachment, with that voucher */ d = call(reqf("{\"v\":1,\"id\":\"61j\",\"cmd\":\"attachment.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"voucher_id\":%lld}}", g_session, (int)org_id, (long long)rent_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); CHECK(jint(d, "result.items.0.id") == att_id); CHECK(jint(d, "result.items.0.voucher_id") == rent_id); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"61k\",\"cmd\":\"attachment.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"voucher_id\":%lld}}", g_session, (int)org_id, (long long)sale_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) == 1); CHECK(jint(d, "result.items.0.id") == att_id); CHECK(jint(d, "result.items.0.voucher_id") == sale_id); yyjson_doc_free(d); /* voucher.list reports the attachment count */ d = call(reqf("{\"v\":1,\"id\":\"61i\",\"cmd\":\"voucher.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"limit\":5}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(jint(d, "result.items.0.attachment_count") >= 1); CHECK(jint(d, "result.items.1.attachment_count") == 0); CHECK(jint(d, "result.items.2.attachment_count") >= 1); yyjson_doc_free(d); /* correction */ d = call(reqf("{\"v\":1,\"id\":\"62\",\"cmd\":\"voucher.correct\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"voucher\":%lld," "\"description\":\"Rättelse felaktig försäljning\"," "\"client_ref\":\"ref-3\"}}", g_session, (int)org_id, (long long)sale_id)); CHECK_OK(d); CHECK(jint(d, "result.corrects_voucher_id") == sale_id); int64_t corr_id = jint(d, "result.id"); CHECK(corr_id > sale_id); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"63\",\"cmd\":\"voucher.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", g_session, (int)org_id, (long long)sale_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.rows")) == 3); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"64\",\"cmd\":\"report.vat\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-01-01\",\"to\":\"2026-01-31\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(vat_box(d, "05") == 50000); CHECK(vat_box(d, "10") == 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"65\",\"cmd\":\"report.income_statement\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK(jint(d, "result.result_ore") == 40000); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"66\",\"cmd\":\"report.balance_sheet\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK(find_amount(d, "result.assets.accounts", "account", "1930", "amount_ore") == 40000); CHECK(jint(d, "result.assets.total_ore") == jint(d, "result.equity.total_ore")); yyjson_doc_free(d); /* attachment size limit is enforced by the handler */ { char big[600]; memset(big, 'A', 500); big[500] = '\0'; g_cfg.max_attachment_bytes = 100; d = call(reqf("{\"v\":1,\"id\":\"76\",\"cmd\":\"attachment.put\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"filename\":\"big.bin\",\"mime\":\"application/octet-stream\"," "\"content_base64\":\"%s\"}}", g_session, (int)org_id, big)); CHECK_STR(d, "error.code", "TOO_LARGE"); yyjson_doc_free(d); g_cfg.max_attachment_bytes = 10 * 1024 * 1024; } /* ---------------- templates (konteringsmallar) ------------------- */ d = call(reqf("{\"v\":1,\"id\":\"80\",\"cmd\":\"template.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":\"Testmall\"," "\"description\":\"Försäljning {x}\",\"rows\":[" "{\"account\":\"1930\",\"formula\":\"x\"}," "{\"account\":\"3001\",\"formula\":\"-x/1.25\"}," "{\"account\":\"2611\",\"formula\":\"-x/5\"}]}}", g_session, (int)org_id)); CHECK_OK(d); int64_t tpl_id = jint(d, "result.id"); CHECK(tpl_id > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"81\",\"cmd\":\"template.list\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"active_only\":true}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.items")) >= 1); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"82\",\"cmd\":\"template.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"name\":\"Testmall\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(yyjson_arr_size(jget(d, "result.rows")) == 3); CHECK_STR(d, "result.rows.0.formula", "x"); CHECK_STR(d, "result.rows.1.account", "3001"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"83\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"template\":\"Testmall\",\"x\":\"1250\"," "\"date\":\"2026-03-05\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(jint(d, "result.rows.0.debit_ore") == 125000); CHECK(jint(d, "result.rows.1.credit_ore") == 100000); CHECK(jint(d, "result.rows.2.credit_ore") == 25000); CHECK_STR(d, "result.description", "Försäljning 1250.00"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"84\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"template\":\"Testmall\",\"x\":\"1250\"," "\"date\":\"2026-03-05\",\"client_ref\":\"tplref\"}}", g_session, (int)org_id)); CHECK_OK(d); int64_t tpl_voucher = jint(d, "result.id"); CHECK(tpl_voucher > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"85\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"template\":\"Testmall\",\"x\":\"1250\"," "\"date\":\"2026-03-05\",\"client_ref\":\"tplref\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(jbool(d, "result.replayed")); yyjson_doc_free(d); /* rounding: four credits of x/4 do not add up to x in öre */ d = call(reqf("{\"v\":1,\"id\":\"86\",\"cmd\":\"template.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"name\":\"Rundning\",\"rows\":[" "{\"account\":\"1930\",\"formula\":\"x\"}," "{\"account\":\"3001\",\"formula\":\"-x/4\"}," "{\"account\":\"2611\",\"formula\":\"-x/4\"}," "{\"account\":\"2641\",\"formula\":\"-x/4\"}," "{\"account\":\"5011\",\"formula\":\"-x/4\"}]}}", g_session, (int)org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"87\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"template\":\"Rundning\",\"x\":\"0.03\"," "\"date\":\"2026-03-06\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK(jint(d, "result.rows.0.debit_ore") == 4); CHECK(jint(d, "result.rows.1.credit_ore") == 1); CHECK(jint(d, "result.rows.4.credit_ore") == 1); yyjson_doc_free(d); /* invalid formula and unknown account are rejected at save time */ d = call(reqf("{\"v\":1,\"id\":\"88\",\"cmd\":\"template.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":\"Fel1\"," "\"rows\":[{\"account\":\"1930\",\"formula\":\"x**\"}," "{\"account\":\"3001\",\"formula\":\"-x\"}]}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"89\",\"cmd\":\"template.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":\"Fel2\"," "\"rows\":[{\"account\":\"9999\",\"formula\":\"x\"}," "{\"account\":\"3001\",\"formula\":\"-x\"}]}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "ACCOUNT_NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"90\",\"cmd\":\"template.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"name\":\"Testmall 2\"}}", g_session, (int)org_id, (long long)tpl_id)); CHECK_OK(d); CHECK_STR(d, "result.name", "Testmall 2"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"91\",\"cmd\":\"template.archive\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"name\":\"Testmall 2\"}}", g_session, (int)org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"92\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"template\":\"Testmall 2\",\"x\":\"100\"," "\"date\":\"2026-03-05\"}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); /* ---------------- ingående balans (series IB) -------------------- */ d = call(reqf("{\"v\":1,\"id\":\"93\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":\"%s\"," "\"description\":\"Ingående balans\",\"series\":\"IB\"," "\"rows\":[{\"account\":\"1930\",\"debit_ore\":50000}," "{\"account\":\"2010\",\"credit_ore\":50000}]}}", g_session, (int)org_id, "2026-01-01")); CHECK_OK(d); CHECK_STR(d, "result.series", "IB"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"94\",\"cmd\":\"report.trial_balance\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK(find_amount(d, "result.accounts", "account", "1930", "ib_ore") == 50000); CHECK(find_amount(d, "result.accounts", "account", "2010", "ib_ore") == -50000); yyjson_doc_free(d); /* period lock */ d = call(reqf("{\"v\":1,\"id\":\"67\",\"cmd\":\"period.lock\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" "%lld,\"until\":\"2026-01-31\",\"reason\":\"bokslut\"}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"68\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"date\":\"2026-01-15\",\"description\":\"Låst\"," "\"rows\":[{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "PERIOD_LOCKED"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"69\",\"cmd\":\"period.unlock\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" "%lld,\"reason\":\"test\"}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); yyjson_doc_free(d); /* SIE export */ d = call(reqf("{\"v\":1,\"id\":\"70\",\"cmd\":\"sie.export\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"fiscal_year\":" "%lld,\"inline\":true}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); CHECK(jint(d, "result.size") > 0); CHECK(jstr(d, "result.sha256") && strlen(jstr(d, "result.sha256")) == 64); char sie_path[512] = ""; snprintf(sie_path, sizeof sie_path, "%s", jstr(d, "result.path")); char *sie_b64 = xstrdup(jstr(d, "result.content_base64")); yyjson_doc_free(d); { FILE *sf = fopen(sie_path, "rb"); CHECK(sf != NULL); if (sf) { char sbuf[65536]; size_t sn = fread(sbuf, 1, sizeof sbuf - 1, sf); sbuf[sn] = '\0'; fclose(sf); CHECK(strstr(sbuf, "#SIETYP 4") != NULL); CHECK(strstr(sbuf, "#VER") != NULL); CHECK(strstr(sbuf, "#TRANS 1930") != NULL); } } /* SIE import into a second org */ d = call(reqf("{\"v\":1,\"id\":\"71\",\"cmd\":\"org.create\"," "\"session\":\"%s\",\"args\":{\"name\":\"AB Två\"}}", g_session)); CHECK_OK(d); int64_t org2 = jint(d, "result.id"); CHECK(jint(d, "result.fiscal_year_id") > 0); yyjson_doc_free(d); d = call_sie_import(g_session, org2, sie_b64, 1); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); CHECK(jint(d, "result.vouchers") >= 4); yyjson_doc_free(d); d = call_sie_import(g_session, org2, sie_b64, 0); CHECK_OK(d); CHECK(jint(d, "result.vouchers") >= 4); yyjson_doc_free(d); free(sie_b64); d = call(reqf("{\"v\":1,\"id\":\"72\",\"cmd\":\"report.trial_balance\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org2)); CHECK_OK(d); int64_t ub_org2 = find_amount(d, "result.accounts", "account", "1930", "ub_ore"); int64_t ib_org2 = find_amount(d, "result.accounts", "account", "1930", "ib_ore"); yyjson_doc_free(d); CHECK(ib_org2 == 50000); d = call(reqf("{\"v\":1,\"id\":\"72b\",\"cmd\":\"report.trial_balance\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); int64_t ub_org1 = find_amount(d, "result.accounts", "account", "1930", "ub_ore"); yyjson_doc_free(d); CHECK(ub_org1 > 0); CHECK(ub_org1 == ub_org2); d = call_sie_import(g_session, org2, "AAAA", 0); CHECK(!jbool(d, "ok")); yyjson_doc_free(d); /* CRLF, #RAR -1 ignored, zero-amount rows dropped */ { static const char sie_crlf[] = "#FLAGGA 0\r\n" "#FORMAT PC8\r\n" "#SIETYP 4\r\n" "#FNAMN \"CRLF AB\"\r\n" "#RAR 0 20240101 20241231\r\n" "#RAR -1 20230101 20231231\r\n" "#KONTO 1930 \"Företagskonto\"\r\n" "#KONTO 2010 \"Eget kapital\"\r\n" "#VER \"A\" \"1\" 20240201 \"Insättning\"\r\n" "{\r\n" "#TRANS 1930 {} 1000.00\r\n" "#TRANS 2010 {} -1000.00\r\n" "#TRANS 2010 {} 0.00\r\n" "}\r\n"; char *b64 = util_b64((const unsigned char *)sie_crlf, strlen(sie_crlf)); d = call_sie_import(g_session, org2, b64, 0); CHECK_OK(d); CHECK(jint(d, "result.vouchers") == 1); int64_t fy_a = jint(d, "result.fiscal_year_id"); yyjson_doc_free(d); free(b64); d = call(reqf("{\"v\":1,\"id\":\"77a\",\"cmd\":\"fiscal_year.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", g_session, (int)org2, (long long)fy_a)); CHECK_OK(d); CHECK_STR(d, "result.start_date", "2024-01-01"); CHECK_STR(d, "result.end_date", "2024-12-31"); yyjson_doc_free(d); } /* a later year's #IB must not duplicate carried balances */ { static const char sie_year2[] = "#FLAGGA 0\n" "#FORMAT PC8\n" "#SIETYP 4\n" "#FNAMN \"CRLF AB\"\n" "#RAR 0 20250101 20251231\n" "#IB 0 1930 1000.00\n" "#IB 0 2010 -1000.00\n" "#VER \"A\" \"1\" 20250201 \"Insättning\"\n" "{\n" "#TRANS 1930 {} 50.00\n" "#TRANS 2010 {} -50.00\n" "}\n"; char *b64 = util_b64((const unsigned char *)sie_year2, strlen(sie_year2)); d = call_sie_import(g_session, org2, b64, 0); CHECK_OK(d); int64_t fy_b = jint(d, "result.fiscal_year_id"); yyjson_doc_free(d); free(b64); d = call(reqf("{\"v\":1,\"id\":\"77b\",\"cmd\":\"report.trial_balance\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"fiscal_year\":%lld}}", g_session, (int)org2, (long long)fy_b)); CHECK_OK(d); CHECK(find_amount(d, "result.accounts", "account", "1930", "ib_ore") == 100000); CHECK(find_amount(d, "result.accounts", "account", "1930", "ub_ore") == 105000); yyjson_doc_free(d); } /* ---------------- settings: default series ----------------------- */ d = call(reqf("{\"v\":1,\"id\":\"95\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.default_series", "A"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"96\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"default_series\",\"value\":\"V-\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "V-"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"96b\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.default_series", "V-"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"97\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":\"%s\"," "\"description\":\"Serietest\",\"client_ref\":\"sertest\"," "\"rows\":[{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", g_session, (int)org_id, "2026-04-01")); CHECK_OK(d); CHECK_STR(d, "result.series", "V-"); CHECK(jint(d, "result.number") == 1); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"98\",\"cmd\":\"template.create\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"name\":\"Seriemall\",\"rows\":[" "{\"account\":\"1930\",\"formula\":\"x\"}," "{\"account\":\"3001\",\"formula\":\"-x\"}]}}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.series", "V-"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"99\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"default_series\",\"value\":\"123456789\"}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"100\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"nope\",\"value\":\"x\"}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "UNSUPPORTED"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"99b\",\"cmd\":\"settings.set\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"key\":\"attachment_dir\",\"value\":\"/tmp/bilagor\"}}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.value", "/tmp/bilagor"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"101\",\"cmd\":\"settings.get\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)org_id)); CHECK_OK(d); CHECK_STR(d, "result.attachment_dir", "/tmp/bilagor"); yyjson_doc_free(d); /* ---------------- moms rules: ranges merge per box ------------- */ d = call(reqf("{\"v\":1,\"id\":\"110\",\"cmd\":\"org.create\"," "\"session\":\"%s\",\"args\":{\"name\":\"Moms AB\"}}", g_session)); CHECK_OK(d); int64_t vat_org = jint(d, "result.id"); CHECK(vat_org > 0); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"111\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" "\"2026-02-01\",\"description\":\"Försäljning A\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":125000}," "{\"account\":\"3001\",\"credit_ore\":100000}," "{\"account\":\"2611\",\"credit_ore\":25000}]}}", g_session, (int)vat_org)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"112\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" "\"2026-02-02\",\"description\":\"Försäljning B\",\"rows\":[" "{\"account\":\"1930\",\"debit_ore\":62500}," "{\"account\":\"3105\",\"credit_ore\":50000}," "{\"account\":\"2611\",\"credit_ore\":12500}]}}", g_session, (int)vat_org)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"113\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" "\"2026-02-03\",\"description\":\"EU-inköp\",\"rows\":[" "{\"account\":\"4515\",\"debit_ore\":10000}," "{\"account\":\"2645\",\"debit_ore\":2500}," "{\"account\":\"2614\",\"credit_ore\":2500}," "{\"account\":\"2410\",\"credit_ore\":10000}]}}", g_session, (int)vat_org)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"114\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":" "\"2026-02-04\",\"description\":\"IT-tjänst\",\"rows\":[" "{\"account\":\"6540\",\"debit_ore\":8000}," "{\"account\":\"2641\",\"debit_ore\":2000}," "{\"account\":\"2410\",\"credit_ore\":10000}]}}", g_session, (int)vat_org)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"115\",\"cmd\":\"report.vat\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"from\":\"2026-02-01\",\"to\":\"2026-02-28\"}}", g_session, (int)vat_org)); CHECK_OK(d); CHECK(vat_box(d, "05") == 150000); CHECK(vat_box(d, "10") == 37500); CHECK(vat_box(d, "20") == 10000); CHECK(vat_box(d, "30") == 2500); CHECK(vat_box(d, "48") == -4500); CHECK(vat_box(d, "49") == 35500); int n05 = 0; yyjson_val *boxes = jget(d, "result.boxes"); for (size_t i = 0; i < yyjson_arr_size(boxes); i++) { yyjson_val *box = yyjson_obj_get(yyjson_arr_get(boxes, i), "box"); if (box && yyjson_is_str(box) && strcmp(yyjson_get_str(box), "05") == 0) n05++; } CHECK(n05 == 1); yyjson_doc_free(d); /* ---------------- bokslut: dispositions, tax, transfer ---------- */ d = call(reqf("{\"v\":1,\"id\":\"120\",\"cmd\":\"bokslut.post\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":" "{\"tax_rate\":20.6}}", g_session, (int)vat_org)); CHECK_OK(d); CHECK(jint(d, "result.result_before_ore") == 132000); CHECK(jint(d, "result.taxable_ore") == 132000); CHECK(jint(d, "result.tax_ore") == 27192); CHECK(jint(d, "result.result_after_ore") == 104808); CHECK(yyjson_arr_size(jget(d, "result.vouchers")) == 2); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"121\",\"cmd\":\"bokslut.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"tax_rate\":20.6,\"periodiseringsfond_ore\":50000}}", g_session, (int)vat_org)); CHECK_OK(d); CHECK(jint(d, "result.entries_ore") == -50000); CHECK(jint(d, "result.taxable_ore") == 82000); CHECK(jint(d, "result.tax_ore") == 16892); CHECK(jint(d, "result.result_after_ore") == 65108); CHECK(yyjson_arr_size(jget(d, "result.vouchers")) == 3); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"122\",\"cmd\":\"report.balance_sheet\"," "\"session\":\"%s\",\"org\":%d}", g_session, (int)vat_org)); CHECK_OK(d); CHECK(find_amount(d, "result.equity.accounts", "account", "2110", "amount_ore") == 50000); CHECK(find_amount(d, "result.liabilities.accounts", "account", "2512", "amount_ore") == 16892); CHECK(find_amount(d, "result.equity.accounts", "account", "2099", "amount_ore") == 65108); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"123\",\"cmd\":\"bokslut.post\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"entries\":[{\"debit_account\":\"8910\"," "\"amount_ore\":100}]}}", g_session, (int)vat_org)); CHECK_STR(d, "error.code", "INVALID_ARGS"); yyjson_doc_free(d); /* ---------------- SRU (INK2/INK2R/INK2S) ----------------------- */ d = call(reqf("{\"v\":1,\"id\":\"124\",\"cmd\":\"org.update\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"org_nr\":\"559331-2126\",\"postal_code\":\"192 48\"," "\"city\":\"Sollentuna\"}}", g_session, (int)vat_org)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"125\",\"cmd\":\"sru.export\"," "\"session\":\"%s\",\"org\":%d,\"args\":" "{\"adjustments\":[{\"code\":\"7653\"," "\"amount_ore\":10000}]}}", g_session, (int)vat_org)); CHECK_OK(d); CHECK_STR(d, "result.period", "2026P4"); CHECK_STR(d, "result.blankett", "INK2-2026P4"); char *sb64 = jstr(d, "result.blanketter.content_base64"); unsigned char *srub = NULL; size_t srul = 0; CHECK(sb64 && util_b64_decode(sb64, strlen(sb64), &srub, &srul) == 0); char *sru = xmalloc(srul + 1); memcpy(sru, srub, srul); sru[srul] = '\0'; free(srub); CHECK(strstr(sru, "#BLANKETT INK2-2026P4") != NULL); CHECK(strstr(sru, "#BLANKETT INK2R-2026P4") != NULL); CHECK(strstr(sru, "#BLANKETT INK2S-2026P4") != NULL); CHECK(strstr(sru, "#UPPGIFT 7011 20260101") != NULL); CHECK(strstr(sru, "#UPPGIFT 7410 1500") != NULL); CHECK(strstr(sru, "#UPPGIFT 7513 80") != NULL); CHECK(strstr(sru, "#UPPGIFT 7525 500") != NULL); CHECK(strstr(sru, "#UPPGIFT 7528 168") != NULL); CHECK(strstr(sru, "#UPPGIFT 7450 651") != NULL); CHECK(strstr(sru, "#UPPGIFT 7281 1875") != NULL); CHECK(strstr(sru, "#UPPGIFT 7369 355") != NULL); CHECK(strstr(sru, "#UPPGIFT 7321 500") != NULL); CHECK(strstr(sru, "#UPPGIFT 7302 651") != NULL); CHECK(strstr(sru, "#UPPGIFT 7650 651") != NULL); CHECK(strstr(sru, "#UPPGIFT 7651 168") != NULL); CHECK(strstr(sru, "#UPPGIFT 7653 100") != NULL); CHECK(strstr(sru, "#UPPGIFT 7670 919") != NULL); CHECK(strstr(sru, "#UPPGIFT 7104 919") != NULL); CHECK(strstr(sru, "#FIL_SLUT") != NULL); free(sru); CHECK_STR(d, "result.from", "2026-01-01"); CHECK(find_amount(d, "result.ink2r", "code", "7281", "amount") == 1875); CHECK(find_amount(d, "result.ink2s", "code", "7670", "amount") == 919); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"126\",\"cmd\":\"sru.export\"," "\"session\":\"%s\",\"org\":%d,\"args\":{}}", g_session, (int)vat_org)); CHECK_OK(d); sb64 = jstr(d, "result.info.content_base64"); srub = NULL; srul = 0; CHECK(sb64 && util_b64_decode(sb64, strlen(sb64), &srub, &srul) == 0); sru = xmalloc(srul + 1); memcpy(sru, srub, srul); sru[srul] = '\0'; free(srub); CHECK(strstr(sru, "#ORGNR 165593312126") != NULL); CHECK(strstr(sru, "#POSTORT Sollentuna") != NULL); CHECK(strstr(sru, "#FILNAMN BLANKETTER.SRU") != NULL); free(sru); yyjson_doc_free(d); /* close fiscal year and verify hard stop */ d = call(reqf("{\"v\":1,\"id\":\"73\",\"cmd\":\"fiscal_year.close\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"confirm\":true}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"74\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"date\":\"2026-03-01\",\"description\":\"Stängt\"," "\"rows\":[{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", g_session, (int)org_id)); CHECK_STR(d, "error.code", "FISCAL_YEAR_CLOSED"); yyjson_doc_free(d); /* reopen is the owner-only undo */ d = call(reqf("{\"v\":1,\"id\":\"76\",\"cmd\":\"fiscal_year.reopen\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"id\":%lld,\"confirm\":true}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); CHECK(jbool(d, "result.dry_run")); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"77\",\"cmd\":\"fiscal_year.reopen\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"confirm\":true}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"78\",\"cmd\":\"fiscal_year.get\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}", g_session, (int)org_id, (long long)fy1)); CHECK_OK(d); CHECK_STR(d, "result.status", "open"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"79\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%d,\"dry_run\":true," "\"args\":{\"date\":\"2026-03-01\",\"description\":\"Öppet\"," "\"rows\":[{\"account\":\"1930\",\"debit_ore\":100}," "{\"account\":\"3001\",\"credit_ore\":100}]}}", g_session, (int)org_id)); CHECK_OK(d); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"79b\",\"cmd\":\"fiscal_year.reopen\"," "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld," "\"confirm\":true}}", g_session, (int)org_id, (long long)fy1)); CHECK_STR(d, "error.code", "NOT_FOUND"); yyjson_doc_free(d); d = call(reqf("{\"v\":1,\"id\":\"75\",\"cmd\":\"audit.verify\"," "\"session\":\"%s\"}", g_session)); CHECK_OK(d); CHECK(jbool(d, "result.ok")); CHECK(jint(d, "result.checked") > 20); yyjson_doc_free(d); test_transport(); /* rate limiting must stay last: it blocks the login key */ for (int i = 0; i < 5; i++) CHECK(!login("admin", "wrong")); d = call("{\"v\":1,\"id\":\"32\",\"cmd\":\"session.open\",\"args\":" "{\"method\":\"password\",\"username\":\"admin\"," "\"password\":\"secret123\"}}"); CHECK_STR(d, "error.code", "RATE_LIMITED"); yyjson_doc_free(d); sqlite3_close(g_db); char path[600]; snprintf(path, sizeof path, "%s-wal", dbpath); unlink(path); snprintf(path, sizeof path, "%s-shm", dbpath); unlink(path); unlink(dbpath); rmdir(backupdir); rmdir(tmpdir); config_free(); if (g_failures) { fprintf(stderr, "%d check(s) failed\n", g_failures); return 1; } printf("all tests passed\n"); return 0; }