aboutsummaryrefslogtreecommitdiff
path: root/src/commands.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-18 22:16:32 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-18 22:16:32 +0200
commite9fc91dbc8314a25a0d71b7ee4344d3fc4ccfd5e (patch)
treed5c14087df9db17c63b5be5cada4f11592c493b3 /src/commands.c
parent251eb8c54c77029f0b43aa987ea50c44016147f3 (diff)
downloadbokf-e9fc91dbc8314a25a0d71b7ee4344d3fc4ccfd5e.tar.gz
bokf-e9fc91dbc8314a25a0d71b7ee4344d3fc4ccfd5e.zip
attachments: link/unlink from the TUI, multi-voucher linksv0.1.29
The voucher detail can attach a file to an existing voucher (^F) and the underlag picker (f) can remove a link (d, confirmed). The Underlag inbox links a highlighted item to a voucher picked from a list (k). attachment.link and attachment.unlink are new write commands (dry-run, audited); unlinked files return to the inbox. The same content may now link to several vouchers — only the same voucher/attachment pair is a CONFLICT, matching the table's primary key.
Diffstat (limited to 'src/commands.c')
-rw-r--r--src/commands.c87
1 files changed, 81 insertions, 6 deletions
diff --git a/src/commands.c b/src/commands.c
index e16ba75..d7f3860 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -3185,14 +3185,15 @@ static yyjson_mut_val *h_voucher_correct(struct req *r)
/* ------------------------------------------------------------------ */
static int link_attachment(struct req *r, int64_t aid, int64_t vid,
- char **errmsg)
+ 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 attachment_id=?3),"
+ " 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");
@@ -3210,14 +3211,17 @@ static int link_attachment(struct req *r, int64_t aid, int64_t vid,
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];
@@ -3290,8 +3294,10 @@ static yyjson_mut_val *h_attachment_put(struct req *r)
free(content);
if (voucher_id) {
char *emsg = NULL;
- if (link_attachment(r, existing, voucher_id, &emsg) != 0) {
- yyjson_mut_val *res = fail(r, "CONFLICT", emsg);
+ 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;
}
@@ -3331,8 +3337,9 @@ static yyjson_mut_val *h_attachment_put(struct req *r)
if (voucher_id) {
char *emsg = NULL;
- if (link_attachment(r, id, voucher_id, &emsg) != 0) {
- yyjson_mut_val *res = fail(r, "NOT_FOUND", emsg);
+ 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;
}
@@ -3347,6 +3354,70 @@ static yyjson_mut_val *h_attachment_put(struct req *r)
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;
@@ -3844,6 +3915,10 @@ const struct command g_commands[] = {
h_template_archive },
{ "attachment.put", "Store an underlag (receipt, invoice)", PERM_WRITE, 1,
1, 1, h_attachment_put },
+ { "attachment.link", "Link an existing attachment to a voucher",
+ PERM_WRITE, 1, 1, 1, h_attachment_link },
+ { "attachment.unlink", "Remove an attachment link", PERM_WRITE, 1, 1, 1,
+ h_attachment_unlink },
{ "attachment.get", "Fetch an attachment", PERM_READ, 1, 0, 0,
h_attachment_get },
{ "attachment.list", "List attachments / inbox", PERM_READ, 1, 0, 0,