summaryrefslogtreecommitdiff
path: root/tests/test_core.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 12:59:05 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 12:59:05 +0200
commite5abd28a9806ee8f4239a1db068876be82363975 (patch)
tree2af68cbd748e49f7547226c06b233240a2f252c5 /tests/test_core.c
parent9ee8cb18b1017dce11e7c8c9b7fafad301cb766e (diff)
downloadbokf-e5abd28a9806ee8f4239a1db068876be82363975.tar.gz
bokf-e5abd28a9806ee8f4239a1db068876be82363975.zip
bank: import SEB CSV and match against vouchers (schema v8)
Diffstat (limited to 'tests/test_core.c')
-rw-r--r--tests/test_core.c254
1 files changed, 254 insertions, 0 deletions
diff --git a/tests/test_core.c b/tests/test_core.c
index 70a1ab7..a78a940 100644
--- a/tests/test_core.c
+++ b/tests/test_core.c
@@ -192,6 +192,23 @@ static yyjson_doc *call_sie_import(const char *session, int64_t org,
return d;
}
+static yyjson_doc *call_bank_import(const char *session, int64_t org,
+ const char *csv, int dry)
+{
+ char *b64 = util_b64((const unsigned char *)csv, strlen(csv));
+ size_t len = strlen(b64) + 1024;
+ char *req = xmalloc(len);
+ snprintf(req, len,
+ "{\"v\":1,\"id\":\"bank\",\"cmd\":\"bank.import\",\"session\":"
+ "\"%s\",\"org\":%lld%s,\"args\":{\"format\":\"seb\","
+ "\"content_base64\":\"%s\"}}",
+ session, (long long)org, dry ? ",\"dry_run\":true" : "", b64);
+ yyjson_doc *d = call(req);
+ free(req);
+ free(b64);
+ return d;
+}
+
static int login(const char *user, const char *pass)
{
yyjson_doc *d = call(reqf(
@@ -521,6 +538,14 @@ static void test_pre_migration_snapshot(const char *tmpdir,
" AND name='attachments_no_delete'");
CHECK(v && strcmp(v, "attachments_no_delete") == 0);
free(v);
+ v = db_text(db, "SELECT name FROM sqlite_master WHERE type='table'"
+ " AND name='bank_transactions'");
+ CHECK(v && strcmp(v, "bank_transactions") == 0);
+ free(v);
+ v = db_text(db, "SELECT name FROM sqlite_master WHERE type='table'"
+ " AND name='bank_matches'");
+ CHECK(v && strcmp(v, "bank_matches") == 0);
+ free(v);
int64_t user_id = 0;
CHECK(db_create_user(db, "migrated", "Migrated", "secret123", 0, &user_id,
&err) == 0);
@@ -1878,6 +1903,7 @@ int main(void)
g_session, (int)org_id));
CHECK_OK(d);
CHECK_STR(d, "result.default_series", "A");
+ CHECK_STR(d, "result.bank_account", "1930");
yyjson_doc_free(d);
d = call(reqf("{\"v\":1,\"id\":\"96\",\"cmd\":\"settings.set\","
@@ -1945,6 +1971,234 @@ int main(void)
CHECK_STR(d, "result.attachment_dir", "/tmp/bilagor");
yyjson_doc_free(d);
+ /* ---------------- bank reconciliation (schema v8) ----------------- */
+
+ static const char seb_csv[] =
+ "Bokförd;Valutadatum;Text;Typ;Insättningar;Uttag;Bokfört saldo\n"
+ "2026-05-10;2026-05-10;\"Faktura 123; kund \"\"AB\"\"\";Insättning;"
+ "1250,00;;10000,00\n"
+ "2026-05-11;2026-05-12;Kortköp;Kort;;-2 409,00;7.591,00\n"
+ "2026-05-20;2026-05-21;Lön;Insättning;799376,10;;806967,10\n";
+ static const char seb_one[] =
+ "Bokförd;Valutadatum;Text;Typ;Insättningar;Uttag;Bokfört saldo\n"
+ "2026-06-01;2026-06-01;Test;Insättning;10,00;;10,00\n";
+ static const char seb_bad_header[] =
+ "Bokförd;Datum;Text;Typ;Insättningar;Uttag;Bokfört saldo\n"
+ "2026-06-01;2026-06-01;Test;Insättning;10,00;;10,00\n";
+ static const char seb_bad_amount[] =
+ "Bokförd;Valutadatum;Text;Typ;Insättningar;Uttag;Bokfört saldo\n"
+ "2026-06-01;2026-06-01;Fel;Insättning;12,abc;;10,00\n";
+
+ d = call_bank_import(g_session, org_id, seb_csv, 1);
+ CHECK_OK(d);
+ CHECK(jbool(d, "result.dry_run"));
+ CHECK_STR(d, "result.format", "seb");
+ CHECK_STR(d, "result.account", "1930");
+ CHECK(jint(d, "result.total") == 3);
+ CHECK(jint(d, "result.imported") == 3);
+ CHECK(jint(d, "result.duplicates") == 0);
+ CHECK_STR(d, "result.first_date", "2026-05-10");
+ CHECK_STR(d, "result.last_date", "2026-05-20");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"200b\",\"cmd\":\"bank.list\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 0);
+ CHECK(jint(d, "result.summary.unmatched") == 0);
+ CHECK(jint(d, "result.summary.matched") == 0);
+ yyjson_doc_free(d);
+
+ d = call_bank_import(g_session, org_id, seb_csv, 0);
+ CHECK_OK(d);
+ CHECK(jint(d, "result.total") == 3);
+ CHECK(jint(d, "result.imported") == 3);
+ CHECK(jint(d, "result.duplicates") == 0);
+ yyjson_doc_free(d);
+
+ d = call_bank_import(g_session, org_id, seb_csv, 0);
+ CHECK_OK(d);
+ CHECK(jint(d, "result.imported") == 0);
+ CHECK(jint(d, "result.duplicates") == 3);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"200d\",\"cmd\":\"bank.list\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 3);
+ CHECK(jint(d, "result.items.0.amount_ore") == 125000);
+ CHECK_STR(d, "result.items.0.text", "Faktura 123; kund \"AB\"");
+ CHECK(jint(d, "result.items.0.balance_ore") == 1000000);
+ CHECK(jint(d, "result.items.1.amount_ore") == -240900);
+ CHECK(jint(d, "result.items.1.balance_ore") == 759100);
+ CHECK(jint(d, "result.items.2.amount_ore") == 79937610);
+ CHECK(jint(d, "result.summary.unmatched") == 3);
+ CHECK(jint(d, "result.summary.matched") == 0);
+ CHECK(jint(d, "result.summary.unmatched_amount_ore") == 79821710);
+ CHECK(yyjson_arr_size(jget(d, "result.items.0.suggestions")) == 0);
+ int64_t tx1 = jint(d, "result.items.0.id");
+ CHECK(tx1 > 0);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"200e\",\"cmd\":\"bank.list\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"from\":\"2026-05-11\",\"to\":\"2026-05-19\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 1);
+ CHECK_STR(d, "result.items.0.booked_at", "2026-05-11");
+ CHECK(jint(d, "result.summary.unmatched") == 3);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"201\",\"cmd\":\"voucher.post\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":{\"date\":"
+ "\"2026-05-10\",\"description\":\"Kundbetalning\","
+ "\"client_ref\":\"bankref1\",\"rows\":["
+ "{\"account\":\"1930\",\"debit_ore\":125000},"
+ "{\"account\":\"3001\",\"credit_ore\":125000}]}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ int64_t bank_voucher = jint(d, "result.id");
+ CHECK(bank_voucher > 0);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"202\",\"cmd\":\"bank.list\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items.0.suggestions")) == 1);
+ CHECK(jint(d, "result.items.0.suggestions.0.voucher_id") == bank_voucher);
+ CHECK(jint(d, "result.items.0.suggestions.0.amount_ore") == 125000);
+ CHECK_STR(d, "result.items.0.suggestions.0.date", "2026-05-10");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"203\",\"cmd\":\"bank.match\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"transaction_id\":%lld,\"voucher_id\":%lld}}",
+ g_session, (int)org_id, (long long)tx1,
+ (long long)bank_voucher));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.transaction_id") == tx1);
+ CHECK(jint(d, "result.voucher_id") == bank_voucher);
+ CHECK(jint(d, "result.difference_ore") == 0);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"204\",\"cmd\":\"bank.match\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"transaction_id\":%lld,\"voucher_id\":%lld}}",
+ g_session, (int)org_id, (long long)tx1,
+ (long long)bank_voucher));
+ CHECK_STR(d, "error.code", "CONFLICT");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"205\",\"cmd\":\"bank.list\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"status\":\"matched\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 1);
+ CHECK(jint(d, "result.items.0.id") == tx1);
+ CHECK(yyjson_arr_size(jget(d, "result.items.0.matches")) == 1);
+ CHECK(jint(d, "result.items.0.matches.0.voucher_id") == bank_voucher);
+ CHECK(jint(d, "result.items.0.matches.0.bank_amount_ore") == 125000);
+ CHECK_STR(d, "result.items.0.matches.0.series", "V-");
+ CHECK_STR(d, "result.items.0.matches.0.date", "2026-05-10");
+ CHECK(yyjson_arr_size(jget(d, "result.items.0.suggestions")) == 0);
+ CHECK(jint(d, "result.summary.matched") == 1);
+ CHECK(jint(d, "result.summary.unmatched") == 2);
+ CHECK(jint(d, "result.summary.unmatched_amount_ore") == 79696710);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"206\",\"cmd\":\"bank.unmatch\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"transaction_id\":%lld,\"voucher_id\":%lld}}",
+ g_session, (int)org_id, (long long)tx1,
+ (long long)bank_voucher));
+ CHECK_OK(d);
+ CHECK(jbool(d, "result.unmatched"));
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"207\",\"cmd\":\"bank.unmatch\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"transaction_id\":%lld,\"voucher_id\":%lld}}",
+ g_session, (int)org_id, (long long)tx1,
+ (long long)bank_voucher));
+ CHECK_STR(d, "error.code", "NOT_FOUND");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"208\",\"cmd\":\"bank.match\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"transaction_id\":%lld,\"voucher_id\":%lld}}",
+ g_session, (int)org_id, (long long)tx1,
+ (long long)bank_voucher));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.difference_ore") == 0);
+ yyjson_doc_free(d);
+
+ d = call_bank_import(g_session, org_id, seb_bad_header, 1);
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ CHECK(jstr(d, "error.message") &&
+ strstr(jstr(d, "error.message"), "header mismatch") != NULL);
+ yyjson_doc_free(d);
+
+ d = call_bank_import(g_session, org_id, seb_bad_amount, 1);
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ CHECK(jstr(d, "error.message") &&
+ strstr(jstr(d, "error.message"), "line 2") != NULL);
+ yyjson_doc_free(d);
+
+ {
+ char *bad_b64 =
+ util_b64((const unsigned char *)seb_one, strlen(seb_one));
+ d = call(reqf("{\"v\":1,\"id\":\"209\",\"cmd\":\"bank.import\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"format\":\"seb\",\"content_base64\":\"%s\","
+ "\"account\":\"9999\"}}",
+ g_session, (int)org_id, bad_b64));
+ CHECK_STR(d, "error.code", "ACCOUNT_NOT_FOUND");
+ yyjson_doc_free(d);
+ free(bad_b64);
+ }
+
+ d = call(reqf("{\"v\":1,\"id\":\"210\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"bank_account\",\"value\":\"19a0\"}}",
+ g_session, (int)org_id));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"211\",\"cmd\":\"settings.set\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"key\":\"bank_account\",\"value\":\"1940\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.value", "1940");
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"212\",\"cmd\":\"settings.get\","
+ "\"session\":\"%s\",\"org\":%d}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.bank_account", "1940");
+ yyjson_doc_free(d);
+
+ d = call_bank_import(g_session, org_id, seb_one, 0);
+ CHECK_OK(d);
+ CHECK_STR(d, "result.account", "1940");
+ CHECK(jint(d, "result.imported") == 1);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"213\",\"cmd\":\"bank.list\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"account\":\"1940\"}}",
+ g_session, (int)org_id));
+ CHECK_OK(d);
+ CHECK(yyjson_arr_size(jget(d, "result.items")) == 1);
+ CHECK_STR(d, "result.items.0.account", "1940");
+ 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\"}}",