From 8351fa7752bb8341ef405a37ff7c73142715e981 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Sun, 20 Sep 2026 20:11:19 +0200 Subject: commands: split the command table by domain --- src/cmd_org.c | 753 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 753 insertions(+) create mode 100644 src/cmd_org.c (limited to 'src/cmd_org.c') diff --git a/src/cmd_org.c b/src/cmd_org.c new file mode 100644 index 0000000..4776491 --- /dev/null +++ b/src/cmd_org.c @@ -0,0 +1,753 @@ +#include "commands.h" +#include "cmd_util.h" + +#include +#include +#include + +#include "audit.h" +#include "config.h" +#include "db.h" +#include "seed.h" +#include "util.h" + +/* ------------------------------------------------------------------ */ +/* orgs */ +/* ------------------------------------------------------------------ */ + +static yyjson_mut_val *org_json(struct req *r, sqlite3_stmt *st) +{ + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", sq(sqlite3_column_text(st, 1))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "org_nr", sq(sqlite3_column_text(st, 2))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "vat_nr", sq(sqlite3_column_text(st, 3))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "address", + sq(sqlite3_column_text(st, 4))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "postal_code", + sq(sqlite3_column_text(st, 5))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "city", sq(sqlite3_column_text(st, 6))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "country", + sq(sqlite3_column_text(st, 7))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "email", sq(sqlite3_column_text(st, 8))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "phone", sq(sqlite3_column_text(st, 9))); + yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_start_month", + sqlite3_column_int(st, 10)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "moms_period", + sq(sqlite3_column_text(st, 11))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "framework", + sq(sqlite3_column_text(st, 12))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at", + sq(sqlite3_column_text(st, 13))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "description", + sq(sqlite3_column_text(st, 14))); + yyjson_mut_obj_add_int(r->rdoc, o, "shares", + sqlite3_column_int64(st, 15)); + return o; +} + +#define ORG_COLUMNS \ + "id,name,org_nr,vat_nr,address,postal_code,city,country,email," \ + "phone,fiscal_year_start_month,moms_period,framework,created_at," \ + "description,shares" + +static yyjson_mut_val *h_org_create(struct req *r) +{ + if (!g_cfg.allow_org_create && !r->is_admin) + return fail(r, "FORBIDDEN", "org creation is disabled"); + const char *name = arg_str(r->args, "name"); + if (!name || !*name) + return fail(r, "INVALID_ARGS", "name is required"); + const char *org_nr = arg_str(r->args, "org_nr"); + int64_t fy_month = 1; + arg_int(r->args, "fiscal_year_start_month", &fy_month); + if (fy_month < 1 || fy_month > 12) + return fail(r, "INVALID_ARGS", "fiscal_year_start_month must be 1-12"); + const char *moms = arg_str(r->args, "moms_period"); + if (!moms) + moms = "month"; + if (strcmp(moms, "month") != 0 && strcmp(moms, "quarter") != 0 && + strcmp(moms, "year") != 0) + return fail(r, "INVALID_ARGS", "moms_period must be month, quarter or year"); + const char *framework = arg_str(r->args, "framework"); + if (!framework) + framework = "K2"; + if (strcmp(framework, "K2") != 0 && strcmp(framework, "K3") != 0) + return fail(r, "INVALID_ARGS", "framework must be K2 or K3"); + + char ts[32]; + util_iso8601(util_now(), ts, sizeof ts); + if (db_exec(r->db, "BEGIN IMMEDIATE", NULL) != 0) + return fail(r, "DB_BUSY", "could not start transaction"); + + sqlite3_stmt *st = NULL; + int rc = sqlite3_prepare_v2( + r->db, + "INSERT INTO orgs(name,org_nr,fiscal_year_start_month,moms_period," + "framework,created_at,created_by) VALUES(?1,?2,?3,?4,?5,?6,?7)", + -1, &st, NULL); + if (rc != SQLITE_OK) { + db_exec(r->db, "ROLLBACK", NULL); + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + } + sqlite3_bind_text(st, 1, name, -1, SQLITE_TRANSIENT); + if (org_nr) + sqlite3_bind_text(st, 2, org_nr, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, 2); + sqlite3_bind_int64(st, 3, fy_month); + sqlite3_bind_text(st, 4, moms, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 5, framework, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 6, ts, -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(st, 7, r->sess->user_id); + rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) { + db_exec(r->db, "ROLLBACK", NULL); + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + } + int64_t org_id = db_last_id(r->db); + + rc = sqlite3_prepare_v2( + r->db, + "INSERT INTO memberships(org_id,user_id,role,created_at)" + " VALUES(?1,?2,'owner',?3)", + -1, &st, NULL); + if (rc != SQLITE_OK) { + db_exec(r->db, "ROLLBACK", NULL); + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + } + sqlite3_bind_int64(st, 1, org_id); + sqlite3_bind_int64(st, 2, r->sess->user_id); + sqlite3_bind_text(st, 3, ts, -1, SQLITE_TRANSIENT); + rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) { + db_exec(r->db, "ROLLBACK", NULL); + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + } + int64_t fy_id = 0; + char *seed_err = NULL; + if (seed_org(r->db, org_id, framework, (int)fy_month, &fy_id, + &seed_err) != 0) { + db_exec(r->db, "ROLLBACK", NULL); + yyjson_mut_val *res = + fail(r, "INTERNAL", seed_err ? seed_err : "seeding failed"); + free(seed_err); + return res; + } + if (db_exec(r->db, "COMMIT", NULL) != 0) + return fail(r, "INTERNAL", "commit failed"); + + char *reqjson = audit_args_json(r->args); + audit_append(r->db, org_id, r->sess->user_id, r->sess->token_id, + "org.create", reqjson, "OK", NULL); + free(reqjson); + r->sess->active_org = org_id; + + st = NULL; + if (sqlite3_prepare_v2(r->db, "SELECT " ORG_COLUMNS " FROM orgs WHERE id=?1", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, org_id); + yyjson_mut_val *o = NULL; + if (sqlite3_step(st) == SQLITE_ROW) + o = org_json(r, st); + sqlite3_finalize(st); + if (!o) + return fail(r, "INTERNAL", "created org not found"); + yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_id", fy_id); + return o; +} + +static yyjson_mut_val *h_org_list(struct req *r) +{ + yyjson_mut_val *items = yyjson_mut_arr(r->rdoc); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "SELECT o.id,o.name,o.org_nr,m.role FROM memberships m" + " JOIN orgs o ON o.id=m.org_id WHERE m.user_id=?1 ORDER BY o.id", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->sess->user_id); + while (sqlite3_step(st) == SQLITE_ROW) { + yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items); + yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", + sq(sqlite3_column_text(st, 1))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "org_nr", + sq(sqlite3_column_text(st, 2))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "role", + sq(sqlite3_column_text(st, 3))); + } + sqlite3_finalize(st); + yyjson_mut_val *out = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_val(r->rdoc, out, "items", items); + return out; +} + +static yyjson_mut_val *h_org_get(struct req *r) +{ + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(r->db, "SELECT " ORG_COLUMNS " FROM orgs WHERE id=?1", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + yyjson_mut_val *o = NULL; + if (sqlite3_step(st) == SQLITE_ROW) + o = org_json(r, st); + sqlite3_finalize(st); + if (!o) + return fail(r, "NOT_FOUND", "org not found"); + return o; +} + +static yyjson_mut_val *h_org_update(struct req *r) +{ + const char *name = arg_str(r->args, "name"); + const char *org_nr = arg_str(r->args, "org_nr"); + const char *vat_nr = arg_str(r->args, "vat_nr"); + const char *address = arg_str(r->args, "address"); + const char *postal = arg_str(r->args, "postal_code"); + const char *city = arg_str(r->args, "city"); + const char *country = arg_str(r->args, "country"); + const char *email = arg_str(r->args, "email"); + const char *phone = arg_str(r->args, "phone"); + const char *moms = arg_str(r->args, "moms_period"); + const char *framework = arg_str(r->args, "framework"); + const char *description = arg_str(r->args, "description"); + int64_t fy_month = 0; + arg_int(r->args, "fiscal_year_start_month", &fy_month); + int64_t shares = -1; + arg_int(r->args, "shares", &shares); + if (!name && !org_nr && !vat_nr && !address && !postal && !city && + !country && !email && !phone && !moms && !framework && fy_month == 0 && + !description && shares < 0) + return fail(r, "INVALID_ARGS", "nothing to update"); + if (name && !*name) + return fail(r, "INVALID_ARGS", "name cannot be empty"); + if (fy_month && (fy_month < 1 || fy_month > 12)) + return fail(r, "INVALID_ARGS", "fiscal_year_start_month must be 1-12"); + if (shares < -1) + return fail(r, "INVALID_ARGS", "shares must be >= 0"); + if (moms && strcmp(moms, "month") != 0 && strcmp(moms, "quarter") != 0 && + strcmp(moms, "year") != 0) + return fail(r, "INVALID_ARGS", "moms_period must be month, quarter or year"); + if (framework && strcmp(framework, "K2") != 0 && + strcmp(framework, "K3") != 0) + return fail(r, "INVALID_ARGS", "framework must be K2 or K3"); + + static const char *const keys[10] = { "name", "org_nr", "vat_nr", + "address", "postal_code", "city", + "country", "email", "phone", + "description" }; + const char *texts[10] = { name, org_nr, vat_nr, address, postal, + city, country, email, phone, description }; + + if (r->dry_run) { + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + for (int i = 0; i < 10; i++) + if (texts[i]) + yyjson_mut_obj_add_strcpy(r->rdoc, o, keys[i], texts[i]); + if (fy_month) + yyjson_mut_obj_add_int(r->rdoc, o, "fiscal_year_start_month", + fy_month); + if (moms) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "moms_period", moms); + if (framework) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "framework", framework); + if (shares >= 0) + yyjson_mut_obj_add_int(r->rdoc, o, "shares", shares); + yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); + return o; + } + + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "UPDATE orgs SET" + " name=COALESCE(?2,name), org_nr=COALESCE(?3,org_nr)," + " vat_nr=COALESCE(?4,vat_nr), address=COALESCE(?5,address)," + " postal_code=COALESCE(?6,postal_code), city=COALESCE(?7,city)," + " country=COALESCE(?8,country), email=COALESCE(?9,email)," + " phone=COALESCE(?10,phone), description=COALESCE(?11," + " description)," + " fiscal_year_start_month=CASE WHEN ?12=0" + " THEN fiscal_year_start_month ELSE ?12 END," + " moms_period=COALESCE(?13,moms_period)," + " framework=COALESCE(?14,framework)," + " shares=CASE WHEN ?15<0 THEN shares ELSE ?15 END" + " WHERE id=?1", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + for (int i = 0; i < 10; i++) { + if (texts[i]) + sqlite3_bind_text(st, i + 2, texts[i], -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, i + 2); + } + sqlite3_bind_int64(st, 12, fy_month); + if (moms) + sqlite3_bind_text(st, 13, moms, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, 13); + if (framework) + sqlite3_bind_text(st, 14, framework, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, 14); + sqlite3_bind_int64(st, 15, shares); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "org.update", reqjson, "OK", NULL); + free(reqjson); + + st = NULL; + if (sqlite3_prepare_v2(r->db, "SELECT " ORG_COLUMNS + " FROM orgs WHERE id=?1", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + yyjson_mut_val *o = NULL; + if (sqlite3_step(st) == SQLITE_ROW) + o = org_json(r, st); + sqlite3_finalize(st); + if (!o) + return fail(r, "NOT_FOUND", "org not found"); + return o; +} + +static yyjson_mut_val *h_org_member_list(struct req *r) +{ + yyjson_mut_val *items = yyjson_mut_arr(r->rdoc); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "SELECT u.id,u.username,u.display_name,m.role FROM memberships m" + " JOIN users u ON u.id=m.user_id WHERE m.org_id=?1 ORDER BY u.id", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + while (sqlite3_step(st) == SQLITE_ROW) { + yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items); + yyjson_mut_obj_add_int(r->rdoc, o, "user_id", sqlite3_column_int64(st, 0)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "username", + sq(sqlite3_column_text(st, 1))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "display_name", + sq(sqlite3_column_text(st, 2))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "role", + sq(sqlite3_column_text(st, 3))); + } + sqlite3_finalize(st); + yyjson_mut_val *out = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_val(r->rdoc, out, "items", items); + return out; +} + +static int64_t username_to_id(sqlite3 *db, const char *username) +{ + sqlite3_stmt *st = NULL; + int64_t id = -1; + if (sqlite3_prepare_v2(db, "SELECT id FROM users WHERE username=?1", -1, + &st, NULL) != SQLITE_OK) + return -1; + sqlite3_bind_text(st, 1, username, -1, SQLITE_TRANSIENT); + if (sqlite3_step(st) == SQLITE_ROW) + id = sqlite3_column_int64(st, 0); + sqlite3_finalize(st); + return id; +} + +static int64_t owner_count(sqlite3 *db, int64_t org_id) +{ + sqlite3_stmt *st = NULL; + int64_t n = -1; + if (sqlite3_prepare_v2( + db, "SELECT count(*) FROM memberships WHERE org_id=?1 AND role='owner'", + -1, &st, NULL) != SQLITE_OK) + return -1; + sqlite3_bind_int64(st, 1, org_id); + if (sqlite3_step(st) == SQLITE_ROW) + n = sqlite3_column_int64(st, 0); + sqlite3_finalize(st); + return n; +} + +static int valid_role(const char *role) +{ + return role && (strcmp(role, "owner") == 0 || + strcmp(role, "bookkeeper") == 0 || + strcmp(role, "viewer") == 0); +} + +static yyjson_mut_val *h_org_member_add(struct req *r) +{ + const char *username = arg_str(r->args, "username"); + const char *role = arg_str(r->args, "role"); + if (!username || !valid_role(role)) + return fail(r, "INVALID_ARGS", "username and a valid role are required"); + int64_t uid = username_to_id(r->db, username); + if (uid < 0) + return fail(r, "NOT_FOUND", "user not found"); + char ts[32]; + util_iso8601(util_now(), ts, sizeof ts); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "INSERT INTO memberships(org_id,user_id,role,created_at)" + " VALUES(?1,?2,?3,?4)", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, uid); + sqlite3_bind_text(st, 3, role, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 4, ts, -1, SQLITE_TRANSIENT); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + return fail(r, "CONFLICT", "user is already a member"); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, 0, "member.add", reqjson, + "OK", NULL); + free(reqjson); + return yyjson_mut_obj(r->rdoc); +} + +static yyjson_mut_val *h_org_member_set_role(struct req *r) +{ + const char *username = arg_str(r->args, "username"); + const char *role = arg_str(r->args, "role"); + if (!username || !valid_role(role)) + return fail(r, "INVALID_ARGS", "username and a valid role are required"); + int64_t uid = username_to_id(r->db, username); + if (uid < 0) + return fail(r, "NOT_FOUND", "user not found"); + char *current = db_membership_role(r->db, r->org_id, uid); + if (!current) + return fail(r, "NOT_FOUND", "user is not a member"); + if (strcmp(current, "owner") == 0 && strcmp(role, "owner") != 0 && + owner_count(r->db, r->org_id) <= 1) { + free(current); + return fail(r, "CONFLICT", "cannot demote the last owner"); + } + free(current); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "UPDATE memberships SET role=?1 WHERE org_id=?2 AND user_id=?3", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_text(st, 1, role, -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(st, 2, r->org_id); + sqlite3_bind_int64(st, 3, uid); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, 0, "member.set_role", + reqjson, "OK", NULL); + free(reqjson); + return yyjson_mut_obj(r->rdoc); +} + +static yyjson_mut_val *h_org_member_remove(struct req *r) +{ + const char *username = arg_str(r->args, "username"); + if (!username) + return fail(r, "INVALID_ARGS", "username is required"); + int64_t uid = username_to_id(r->db, username); + if (uid < 0) + return fail(r, "NOT_FOUND", "user not found"); + char *current = db_membership_role(r->db, r->org_id, uid); + if (!current) + return fail(r, "NOT_FOUND", "user is not a member"); + if (strcmp(current, "owner") == 0 && owner_count(r->db, r->org_id) <= 1) { + free(current); + return fail(r, "CONFLICT", "cannot remove the last owner"); + } + free(current); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, "DELETE FROM memberships WHERE org_id=?1 AND user_id=?2", -1, + &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, uid); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, 0, "member.remove", + reqjson, "OK", NULL); + free(reqjson); + return yyjson_mut_obj(r->rdoc); +} + +/* ------------------------------------------------------------------ */ +/* board members (årsredovisning signatures) */ +/* ------------------------------------------------------------------ */ + +static yyjson_mut_val *board_json(struct req *r, sqlite3_stmt *st) +{ + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", + sq(sqlite3_column_text(st, 1))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "title", + sq(sqlite3_column_text(st, 2))); + return o; +} + +static yyjson_mut_val *h_board_list(struct req *r) +{ + yyjson_mut_val *items = yyjson_mut_arr(r->rdoc); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(r->db, + "SELECT id,name,title FROM board_members" + " WHERE org_id=?1 ORDER BY id", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + while (sqlite3_step(st) == SQLITE_ROW) + yyjson_mut_arr_add_val(items, board_json(r, st)); + sqlite3_finalize(st); + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_val(r->rdoc, o, "items", items); + return o; +} + +static yyjson_mut_val *board_get(struct req *r, int64_t id) +{ + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(r->db, + "SELECT id,name,title FROM board_members" + " WHERE org_id=?1 AND id=?2", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, id); + yyjson_mut_val *o = NULL; + if (sqlite3_step(st) == SQLITE_ROW) + o = board_json(r, st); + sqlite3_finalize(st); + if (!o) + return fail(r, "NOT_FOUND", "board member not found"); + return o; +} + +static yyjson_mut_val *h_board_add(struct req *r) +{ + const char *name = arg_str(r->args, "name"); + const char *title = arg_str(r->args, "title"); + if (!name || !*name) + return fail(r, "INVALID_ARGS", "name is required"); + if (!title || !*title) + title = "Styrelseledamot"; + if (r->dry_run) { + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "title", title); + return o; + } + char ts[32]; + util_iso8601(util_now(), ts, sizeof ts); + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(r->db, + "INSERT INTO board_members(org_id,name,title," + "created_at) VALUES(?1,?2,?3,?4)", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 3, title, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 4, ts, -1, SQLITE_TRANSIENT); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + int64_t id = db_last_id(r->db); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "board.add", reqjson, "OK", NULL); + free(reqjson); + return board_get(r, id); +} + +static yyjson_mut_val *h_board_update(struct req *r) +{ + int64_t id = 0; + if (!arg_int(r->args, "id", &id) || id <= 0) + return fail(r, "INVALID_ARGS", "id is required"); + const char *name = arg_str(r->args, "name"); + const char *title = arg_str(r->args, "title"); + if (!name && !title) + return fail(r, "INVALID_ARGS", "nothing to update"); + if (name && !*name) + return fail(r, "INVALID_ARGS", "name cannot be empty"); + if (title && !*title) + return fail(r, "INVALID_ARGS", "title cannot be empty"); + if (r->dry_run) { + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); + yyjson_mut_obj_add_int(r->rdoc, o, "id", id); + if (name) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name); + if (title) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "title", title); + return o; + } + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(r->db, + "UPDATE board_members SET name=COALESCE(?3,name)," + " title=COALESCE(?4,title)" + " WHERE org_id=?1 AND id=?2", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, id); + if (name) + sqlite3_bind_text(st, 3, name, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, 3); + if (title) + sqlite3_bind_text(st, 4, title, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, 4); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + if (sqlite3_changes(r->db) == 0) + return fail(r, "NOT_FOUND", "board member not found"); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "board.update", reqjson, "OK", NULL); + free(reqjson); + return board_get(r, id); +} + +static yyjson_mut_val *h_board_remove(struct req *r) +{ + int64_t id = 0; + if (!arg_int(r->args, "id", &id) || id <= 0) + return fail(r, "INVALID_ARGS", "id is required"); + if (r->dry_run) { + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); + yyjson_mut_obj_add_int(r->rdoc, o, "id", id); + return o; + } + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(r->db, + "DELETE FROM board_members" + " WHERE org_id=?1 AND id=?2", + -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, id); + int rc = sqlite3_step(st); + sqlite3_finalize(st); + if (rc != SQLITE_DONE) + return fail(r, "INTERNAL", sqlite3_errmsg(r->db)); + if (sqlite3_changes(r->db) == 0) + return fail(r, "NOT_FOUND", "board member not found"); + char *reqjson = audit_args_json(r->args); + audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id, + "board.remove", reqjson, "OK", NULL); + free(reqjson); + return yyjson_mut_obj(r->rdoc); +} + + +static const struct cmd_arg args_org_create[] = { + { "name", ARG_STR, 1, NULL, NULL, "Org name" }, + { "org_nr", ARG_STR, 0, NULL, NULL, "Swedish org number" }, + { "fiscal_year_start_month", ARG_INT, 0, "1", NULL, + "Fiscal year start month 1-12" }, + { "moms_period", ARG_ENUM, 0, "month", "month,quarter,year", + "VAT reporting period" }, + { "framework", ARG_ENUM, 0, "K2", "K2,K3", "Accounting framework" }, +}; + +static const struct cmd_arg args_org_update[] = { + { "name", ARG_STR, 0, NULL, NULL, "Org name" }, + { "org_nr", ARG_STR, 0, NULL, NULL, "Swedish org number" }, + { "vat_nr", ARG_STR, 0, NULL, NULL, "VAT number" }, + { "address", ARG_STR, 0, NULL, NULL, "Street address" }, + { "postal_code", ARG_STR, 0, NULL, NULL, "Postal code" }, + { "city", ARG_STR, 0, NULL, NULL, "City" }, + { "country", ARG_STR, 0, NULL, NULL, "Country" }, + { "email", ARG_STR, 0, NULL, NULL, "E-mail" }, + { "phone", ARG_STR, 0, NULL, NULL, "Phone" }, + { "moms_period", ARG_ENUM, 0, NULL, "month,quarter,year", + "VAT reporting period" }, + { "framework", ARG_ENUM, 0, NULL, "K2,K3", "Accounting framework" }, + { "description", ARG_STR, 0, NULL, NULL, "Free-text description" }, + { "fiscal_year_start_month", ARG_INT, 0, NULL, NULL, + "Fiscal year start month 1-12" }, + { "shares", ARG_INT, 0, NULL, NULL, "Number of shares" }, +}; + +static const struct cmd_arg args_member_role[] = { + { "username", ARG_STR, 1, NULL, NULL, "Username" }, + { "role", ARG_ENUM, 1, NULL, "owner,bookkeeper,viewer", + "Membership role" }, +}; + +static const struct cmd_arg args_member_username[] = { + { "username", ARG_STR, 1, NULL, NULL, "Username" }, +}; + +static const struct cmd_arg args_board_add[] = { + { "name", ARG_STR, 1, NULL, NULL, "Board member name" }, + { "title", ARG_STR, 0, "Styrelseledamot", NULL, "Role on the board" }, +}; + +static const struct cmd_arg args_board_update[] = { + { "id", ARG_INT, 1, NULL, NULL, "Board member id" }, + { "name", ARG_STR, 0, NULL, NULL, "Board member name" }, + { "title", ARG_STR, 0, NULL, NULL, "Role on the board" }, +}; + +static const struct cmd_arg args_board_remove[] = { + { "id", ARG_INT, 1, NULL, NULL, "Board member id" }, +}; + +const struct command g_cmd_org[] = { + { "org.create", "Create an org and become its owner", PERM_READ, 0, 1, 1, + h_org_create, CMD_ARGS(args_org_create) }, + { "org.list", "List orgs for the current user", PERM_READ, 0, 0, 0, + h_org_list, NULL, 0 }, + { "org.get", "Get org details", PERM_READ, 1, 0, 0, h_org_get, NULL, 0 }, + { "org.update", "Update org details (owner)", PERM_OWNER, 1, 1, 1, + h_org_update, CMD_ARGS(args_org_update) }, + { "org.member_list", "List org members", PERM_READ, 1, 0, 0, + h_org_member_list, NULL, 0 }, + { "org.member_add", "Add a member to the org", PERM_OWNER, 1, 1, 1, + h_org_member_add, CMD_ARGS(args_member_role) }, + { "org.member_set_role", "Change a member's role", PERM_OWNER, 1, 1, 1, + h_org_member_set_role, CMD_ARGS(args_member_role) }, + { "org.member_remove", "Remove a member from the org", PERM_OWNER, 1, 1, 1, + h_org_member_remove, CMD_ARGS(args_member_username) }, + { "board.list", "List board members", PERM_READ, 1, 0, 0, h_board_list, + NULL, 0 }, + { "board.add", "Add a board member", PERM_OWNER, 1, 1, 1, h_board_add, + CMD_ARGS(args_board_add) }, + { "board.update", "Update a board member", PERM_OWNER, 1, 1, 1, + h_board_update, CMD_ARGS(args_board_update) }, + { "board.remove", "Remove a board member", PERM_OWNER, 1, 1, 1, + h_board_remove, CMD_ARGS(args_board_remove) }, +}; + +const struct cmd_table g_cmd_table_org = { + g_cmd_org, sizeof g_cmd_org / sizeof g_cmd_org[0] +}; -- cgit v1.3