aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 15:23:20 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 15:23:20 +0200
commit2a69643bf580514712276324fb8c4ea9a512494c (patch)
tree734ad2bf1f946b2d22fdcc38935ae136e24a3921 /tests
parentb9dc51446be2b8858cd0a9b41168fafaf115ae3a (diff)
downloadbokf-2a69643bf580514712276324fb8c4ea9a512494c.tar.gz
bokf-2a69643bf580514712276324fb8c4ea9a512494c.zip
invoice: customer, sequence, preview and issue commands
Diffstat (limited to 'tests')
-rw-r--r--tests/test_core.c518
1 files changed, 518 insertions, 0 deletions
diff --git a/tests/test_core.c b/tests/test_core.c
index a78a940..fa7b5e4 100644
--- a/tests/test_core.c
+++ b/tests/test_core.c
@@ -16,6 +16,7 @@
#include "client.h"
#include "config.h"
#include "db.h"
+#include "invoice.h"
#include "protocol.h"
#include "sessions.h"
#include "util.h"
@@ -2199,6 +2200,523 @@ int main(void)
CHECK_STR(d, "result.items.0.account", "1940");
yyjson_doc_free(d);
+ /* ---------------- invoicing: customers, sequence, issue ---------- */
+
+ d = call(reqf("{\"v\":1,\"id\":\"300\",\"cmd\":\"settings.get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.invoice_receivable_account", "1510");
+ CHECK_STR(d, "result.invoice_revenue_account", "3001");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"301\",\"cmd\":\"customer.create\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":"
+ "\"Andra bygg AB\",\"address\":\"Storgatan 1\","
+ "\"postal_code\":\"171 63\",\"city\":\"Solna\","
+ "\"vat_nr\":\"SE559232855201\","
+ "\"email\":\"faktura@andra.se\",\"your_ref\":\"Eric\","
+ "\"payment_days\":30}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ int64_t cust1 = jint(d, "result.id");
+ CHECK(cust1 > 0);
+ CHECK_STR(d, "result.name", "Andra bygg AB");
+ CHECK_STR(d, "result.city", "Solna");
+ CHECK_STR(d, "result.email", "faktura@andra.se");
+ CHECK(jint(d, "result.payment_days") == 30);
+ CHECK(jbool(d, "result.active"));
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"302\",\"cmd\":\"customer.create\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":"
+ "\"Andra bygg AB\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "CONFLICT");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"303\",\"cmd\":\"customer.create\","
+ "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":"
+ "{\"name\":\"Tillfällig AB\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(jbool(d, "result.dry_run"));
+ CHECK_STR(d, "result.name", "Tillfällig AB");
+ CHECK(jint(d, "result.payment_days") == 30);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"304\",\"cmd\":\"customer.create\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"name\":"
+ "\"NZ Bygg AB\",\"city\":\"Bromma\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ int64_t cust2 = jint(d, "result.id");
+ CHECK(cust2 > 0);
+ CHECK(jint(d, "result.payment_days") == 30);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"305\",\"cmd\":\"customer.get\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.name", "Andra bygg AB");
+ CHECK_STR(d, "result.address", "Storgatan 1");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"306\",\"cmd\":\"customer.get\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "NOT_FOUND");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"307\",\"cmd\":\"customer.list\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 2);
+ CHECK_STR(d, "result.items.0.name", "Andra bygg AB");
+ CHECK_STR(d, "result.items.1.name", "NZ Bygg AB");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"308\",\"cmd\":\"customer.update\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld,"
+ "\"city\":\"Sundbyberg\",\"your_ref\":\"Lars\"}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.city", "Sundbyberg");
+ CHECK_STR(d, "result.your_ref", "Lars");
+ CHECK_STR(d, "result.address", "Storgatan 1");
+ CHECK_STR(d, "result.name", "Andra bygg AB");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"309\",\"cmd\":\"customer.update\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld,"
+ "\"name\":\"Andra bygg AB\"}}",
+ g_session, (int)org_id, (long long)cust2));
+ CHECK_STR(d, "error.code", "CONFLICT");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"310\",\"cmd\":\"customer.update\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999,"
+ "\"city\":\"X\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "NOT_FOUND");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"311\",\"cmd\":\"customer.update\","
+ "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":"
+ "{\"id\":%lld,\"city\":\"Nacka\"}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_OK(d);
+ CHECK(jbool(d, "result.dry_run"));
+ CHECK_STR(d, "result.city", "Nacka");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"312\",\"cmd\":\"customer.get\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.city", "Sundbyberg");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"313\",\"cmd\":\"customer.archive\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld,"
+ "\"active\":false}}",
+ g_session, (int)org_id, (long long)cust2));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.id") == cust2);
+ CHECK(!jbool(d, "result.active"));
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"314\",\"cmd\":\"customer.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);
+ CHECK_STR(d, "result.items.0.name", "Andra bygg AB");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"315\",\"cmd\":\"customer.archive\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999,"
+ "\"active\":false}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "NOT_FOUND");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"320\",\"cmd\":\"invoice.sequence_get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.next_number") == 1);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"321\",\"cmd\":\"invoice.sequence_set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"next_number\":"
+ "0}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"322\",\"cmd\":\"invoice.sequence_set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"next_number\":"
+ "17761}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.next_number") == 17761);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"323\",\"cmd\":\"invoice.sequence_get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.next_number") == 17761);
+ yyjson_doc_free(d);
+
+ char draft_args[2048];
+ snprintf(draft_args, sizeof draft_args,
+ "{\"customer_id\":%lld,\"invoice_date\":\"2026-09-20\","
+ "\"due_date\":\"2026-10-20\",\"delivery_date\":\"2026-09-20\","
+ "\"your_ref\":\"Lars\",\"our_ref\":\"Anders\",\"notes\":\"\","
+ "\"rows\":[{\"article_no\":\"\",\"description\":"
+ "\"Utvecklingsarbete\",\"quantity\":\"61\",\"unit\":\"tim\","
+ "\"unit_price_ore\":120000,\"note\":\"\",\"vat_code\":\"25\","
+ "\"account\":\"\"}]}",
+ (long long)cust1);
+
+ d = call(reqf("{\"v\":1,\"id\":\"330\",\"cmd\":\"invoice.preview\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":%s}",
+ g_session, (int)org_id, draft_args));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.number") == 17761);
+ char number_str[32];
+ snprintf(number_str, sizeof number_str, "%lld",
+ (long long)jint(d, "result.number"));
+ char expect_ocr[40];
+ snprintf(expect_ocr, sizeof expect_ocr, "%s%d", number_str,
+ invoice_ocr_check(number_str));
+ CHECK_STR(d, "result.ocr", expect_ocr);
+ CHECK(jint(d, "result.net_ore") == 7320000);
+ CHECK(jint(d, "result.vat_ore") == 1830000);
+ CHECK(jint(d, "result.total_ore") == 9150000);
+ const char *pdf_b64 = jstr(d, "result.content_base64");
+ char *preview_b64 = pdf_b64 ? xstrdup(pdf_b64) : NULL;
+ CHECK(preview_b64 && strncmp(preview_b64, "JVBERi0", 7) == 0);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"331\",\"cmd\":\"invoice.preview\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":%s}",
+ g_session, (int)org_id, draft_args));
+ CHECK_OK(d);
+ CHECK(preview_b64 && jstr(d, "result.content_base64") &&
+ strcmp(jstr(d, "result.content_base64"), preview_b64) == 0);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"332\",\"cmd\":\"invoice.sequence_get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.next_number") == 17761);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"333\",\"cmd\":\"invoice.preview\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":"
+ "\"2026-10-20\",\"rows\":[{\"description\":\"X\","
+ "\"quantity\":\"1\",\"unit_price_ore\":100,"
+ "\"vat_code\":\"99\"}]}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"334\",\"cmd\":\"invoice.preview\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":"
+ "\"2026-10-20\",\"rows\":[{\"description\":\"X\","
+ "\"quantity\":\"1,2345\",\"unit_price_ore\":100,"
+ "\"vat_code\":\"25\"}]}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"335\",\"cmd\":\"invoice.preview\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":"
+ "\"2026-10-20\",\"rows\":[{\"description\":\"X\","
+ "\"quantity\":\"0\",\"unit_price_ore\":100,"
+ "\"vat_code\":\"25\"}]}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"336\",\"cmd\":\"invoice.preview\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "999999,\"invoice_date\":\"2026-09-20\",\"due_date\":"
+ "\"2026-10-20\",\"rows\":[{\"description\":\"X\","
+ "\"quantity\":\"1\",\"unit_price_ore\":100,"
+ "\"vat_code\":\"25\"}]}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "NOT_FOUND");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"337\",\"cmd\":\"invoice.preview\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":"
+ "\"2026-10-20\",\"rows\":[{\"description\":\"X\","
+ "\"quantity\":\"1\",\"unit_price_ore\":100,"
+ "\"vat_code\":\"25\"}]}}",
+ g_session, (int)org_id, (long long)cust2));
+ CHECK_STR(d, "error.code", "NOT_FOUND");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"337a\",\"cmd\":\"invoice.preview\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":"
+ "\"2026-10-20\",\"rows\":[{\"description\":\"X\","
+ "\"quantity\":\"1000\",\"unit_price_ore\":"
+ "9223372036854775807,\"vat_code\":\"25\"}]}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"337b\",\"cmd\":\"invoice.preview\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":"
+ "\"2026-10-20\",\"rows\":[{\"description\":\"X\","
+ "\"quantity\":\"1\",\"unit_price_ore\":-1,"
+ "\"vat_code\":\"25\"}]}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ {
+ char rows_json[8192];
+ size_t off = 0;
+ off += (size_t)snprintf(rows_json + off, sizeof rows_json - off, "[");
+ for (int i = 0; i < 20 && off < sizeof rows_json - 256; i++)
+ off += (size_t)snprintf(
+ rows_json + off, sizeof rows_json - off,
+ "%s{\"description\":\"Rad %d\",\"quantity\":\"1\","
+ "\"unit_price_ore\":100,\"vat_code\":\"25\"}",
+ i ? "," : "", i + 1);
+ snprintf(rows_json + off, sizeof rows_json - off, "]");
+ d = call(reqf("{\"v\":1,\"id\":\"337c\",\"cmd\":\"invoice.preview\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "%lld,\"invoice_date\":\"2026-09-20\",\"due_date\":"
+ "\"2026-10-20\",\"rows\":%s}}",
+ g_session, (int)org_id, (long long)cust1, rows_json));
+ CHECK_STR(d, "error.code", "TOO_LARGE");
+ yyjson_doc_free(d);
+ }
+
+ d = call(reqf("{\"v\":1,\"id\":\"338\",\"cmd\":\"invoice.issue\","
+ "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":"
+ "{\"customer_id\":%lld,\"invoice_date\":\"2026-09-20\","
+ "\"due_date\":\"2026-10-20\",\"rows\":[{\"description\":"
+ "\"X\",\"quantity\":\"1\",\"unit_price_ore\":100,"
+ "\"vat_code\":\"25\",\"account\":\"9999\"}]}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_STR(d, "error.code", "ACCOUNT_NOT_FOUND");
+ yyjson_doc_free(d);
+
+ int64_t vouchers_before = db_count(g_db, "SELECT count(*) FROM vouchers");
+ int64_t attachments_before =
+ db_count(g_db, "SELECT count(*) FROM attachments");
+
+ d = call(reqf("{\"v\":1,\"id\":\"340\",\"cmd\":\"invoice.issue\","
+ "\"session\":\"%s\",\"org\":%d,\"dry_run\":true,\"args\":%s}",
+ g_session, (int)org_id, draft_args));
+ CHECK_OK(d);
+ CHECK(jbool(d, "result.dry_run"));
+ CHECK(jint(d, "result.number") == 17761);
+ CHECK_STR(d, "result.ocr", expect_ocr);
+ CHECK(jint(d, "result.net_ore") == 7320000);
+ CHECK(jint(d, "result.vat_ore") == 1830000);
+ CHECK(jint(d, "result.total_ore") == 9150000);
+ yyjson_val *dry_doc = jget(d, "result.document_id");
+ CHECK(dry_doc && yyjson_is_null(dry_doc));
+ yyjson_doc_free(d);
+ CHECK(db_count(g_db, "SELECT count(*) FROM vouchers") == vouchers_before);
+ CHECK(db_count(g_db, "SELECT count(*) FROM attachments") ==
+ attachments_before);
+
+ d = call(reqf("{\"v\":1,\"id\":\"341\",\"cmd\":\"invoice.sequence_get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.next_number") == 17761);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"350\",\"cmd\":\"invoice.issue\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":%s}",
+ g_session, (int)org_id, draft_args));
+ CHECK_OK(d);
+ int64_t inv1 = jint(d, "result.id");
+ int64_t doc1 = jint(d, "result.document_id");
+ int64_t vou1 = jint(d, "result.voucher_id");
+ CHECK(inv1 > 0);
+ CHECK(doc1 > 0);
+ CHECK(vou1 > 0);
+ CHECK(jint(d, "result.number") == 17761);
+ CHECK_STR(d, "result.ocr", expect_ocr);
+ CHECK(jint(d, "result.net_ore") == 7320000);
+ CHECK(jint(d, "result.vat_ore") == 1830000);
+ CHECK(jint(d, "result.total_ore") == 9150000);
+ yyjson_doc_free(d);
+ CHECK(db_count(g_db, "SELECT count(*) FROM vouchers") == vouchers_before + 1);
+ CHECK(db_count(g_db, "SELECT count(*) FROM attachments") ==
+ attachments_before + 1);
+
+ d = call(reqf("{\"v\":1,\"id\":\"351\",\"cmd\":\"voucher.get\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}",
+ g_session, (int)org_id, (long long)vou1));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.source", "invoice");
+ CHECK_STR(d, "result.date", "2026-09-20");
+ CHECK(find_amount(d, "result.rows", "account", "1510", "debit_ore") ==
+ 9150000);
+ CHECK(find_amount(d, "result.rows", "account", "3001", "credit_ore") ==
+ 7320000);
+ CHECK(find_amount(d, "result.rows", "account", "2610", "credit_ore") ==
+ 1830000);
+ CHECK(find_amount(d, "result.rows", "account", "1510", "credit_ore") == 0);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"352\",\"cmd\":\"invoice.get\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}",
+ g_session, (int)org_id, (long long)inv1));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.status", "issued");
+ CHECK(jint(d, "result.customer_id") == cust1);
+ CHECK_STR(d, "result.customer_name", "Andra bygg AB");
+ CHECK_STR(d, "result.ocr", expect_ocr);
+ CHECK(jint(d, "result.document_id") == doc1);
+ CHECK(jint(d, "result.voucher_id") == vou1);
+ CHECK(yyjson_arr_size(jget(d, "result.rows")) == 1);
+ CHECK(jint(d, "result.rows.0.quantity_milli") == 61000);
+ CHECK(jint(d, "result.rows.0.amount_ore") == 7320000);
+ CHECK_STR(d, "result.rows.0.vat_code", "25");
+ CHECK_STR(d, "result.rows.0.account", "3001");
+ CHECK_STR(d, "result.rows.0.unit", "tim");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"353\",\"cmd\":\"invoice.pdf\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}",
+ g_session, (int)org_id, (long long)inv1));
+ CHECK_OK(d);
+ const char *stored = jstr(d, "result.content_base64");
+ CHECK(stored && preview_b64 && strcmp(stored, preview_b64) == 0);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"355\",\"cmd\":\"invoice.issue\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "%lld,\"invoice_date\":\"2026-09-21\",\"due_date\":"
+ "\"2026-10-21\",\"rows\":[{\"description\":\"Konsult\","
+ "\"quantity\":\"2,5\",\"unit\":\"tim\","
+ "\"unit_price_ore\":100000,\"vat_code\":\"12\","
+ "\"account\":\"3002\"}]}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_OK(d);
+ int64_t inv2 = jint(d, "result.id");
+ int64_t doc2 = jint(d, "result.document_id");
+ int64_t vou2 = jint(d, "result.voucher_id");
+ CHECK(inv2 > inv1);
+ CHECK(doc2 > doc1);
+ CHECK(vou2 > vou1);
+ CHECK(jint(d, "result.number") == 17762);
+ CHECK(jint(d, "result.net_ore") == 250000);
+ CHECK(jint(d, "result.vat_ore") == 30000);
+ CHECK(jint(d, "result.total_ore") == 280000);
+ yyjson_doc_free(d);
+ CHECK(db_count(g_db, "SELECT count(*) FROM vouchers") == vouchers_before + 2);
+ CHECK(db_count(g_db, "SELECT count(*) FROM attachments") ==
+ attachments_before + 2);
+
+ d = call(reqf("{\"v\":1,\"id\":\"356\",\"cmd\":\"voucher.get\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":%lld}}",
+ g_session, (int)org_id, (long long)vou2));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.source", "invoice");
+ CHECK(find_amount(d, "result.rows", "account", "1510", "debit_ore") ==
+ 280000);
+ CHECK(find_amount(d, "result.rows", "account", "2620", "credit_ore") ==
+ 30000);
+ CHECK(find_amount(d, "result.rows", "account", "3002", "credit_ore") ==
+ 250000);
+ CHECK(find_amount(d, "result.rows", "account", "2610", "credit_ore") == -1);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"357\",\"cmd\":\"invoice.list\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 2);
+ CHECK(jint(d, "result.items.0.number") == 17762);
+ CHECK(jint(d, "result.items.1.number") == 17761);
+ CHECK_STR(d, "result.items.0.customer_name", "Andra bygg AB");
+ CHECK_STR(d, "result.items.0.status", "issued");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"358\",\"cmd\":\"invoice.list\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "%lld}}",
+ g_session, (int)org_id, (long long)cust1));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 2);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"359\",\"cmd\":\"invoice.list\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"customer_id\":"
+ "%lld}}",
+ g_session, (int)org_id, (long long)cust2));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 0);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"360\",\"cmd\":\"invoice.list\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"status\":"
+ "\"credited\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 0);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"361\",\"cmd\":\"invoice.list\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"limit\":1}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 1);
+ CHECK(jint(d, "result.items.0.number") == 17762);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"362\",\"cmd\":\"invoice.list\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"status\":"
+ "\"nope\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"363\",\"cmd\":\"invoice.get\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "NOT_FOUND");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"364\",\"cmd\":\"invoice.pdf\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"id\":999999}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "NOT_FOUND");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"365\",\"cmd\":\"invoice.sequence_get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.next_number") == 17763);
+ yyjson_doc_free(d);
+
+ free(preview_b64);
+
/* ---------------- moms rules: ranges merge per box ------------- */
d = call(reqf("{\"v\":1,\"id\":\"110\",\"cmd\":\"org.create\","
"\"session\":\"%s\",\"args\":{\"name\":\"Moms AB\"}}",