diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/commands.c | 336 | ||||
| -rw-r--r-- | src/db.c | 65 | ||||
| -rw-r--r-- | src/db.h | 2 |
3 files changed, 371 insertions, 32 deletions
diff --git a/src/commands.c b/src/commands.c index 62de0be..448d03e 100644 --- a/src/commands.c +++ b/src/commands.c @@ -517,12 +517,17 @@ static yyjson_mut_val *org_json(struct req *r, sqlite3_stmt *st) 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" + "phone,fiscal_year_start_month,moms_period,framework,created_at," \ + "description,shares" static yyjson_mut_val *h_org_create(struct req *r) { @@ -689,15 +694,21 @@ static yyjson_mut_val *h_org_update(struct req *r) 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) + !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"); @@ -705,15 +716,16 @@ static yyjson_mut_val *h_org_update(struct req *r) strcmp(framework, "K3") != 0) return fail(r, "INVALID_ARGS", "framework must be K2 or K3"); - static const char *const keys[9] = { "name", "org_nr", "vat_nr", - "address", "postal_code", "city", - "country", "email", "phone" }; - const char *texts[9] = { name, org_nr, vat_nr, address, postal, - city, country, email, phone }; + 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 < 9; i++) + 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) @@ -723,6 +735,8 @@ static yyjson_mut_val *h_org_update(struct req *r) 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; } @@ -735,30 +749,33 @@ static yyjson_mut_val *h_org_update(struct req *r) " 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)," - " fiscal_year_start_month=CASE WHEN ?11=0" - " THEN fiscal_year_start_month ELSE ?11 END," - " moms_period=COALESCE(?12,moms_period)," - " framework=COALESCE(?13,framework)" + " 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 < 9; i++) { + 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, 11, fy_month); + sqlite3_bind_int64(st, 12, fy_month); if (moms) - sqlite3_bind_text(st, 12, moms, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 13, moms, -1, SQLITE_TRANSIENT); else - sqlite3_bind_null(st, 12); + sqlite3_bind_null(st, 13); if (framework) - sqlite3_bind_text(st, 13, framework, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 14, framework, -1, SQLITE_TRANSIENT); else - sqlite3_bind_null(st, 13); + sqlite3_bind_null(st, 14); + sqlite3_bind_int64(st, 15, shares); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) @@ -953,6 +970,182 @@ static yyjson_mut_val *h_org_member_remove(struct req *r) } /* ------------------------------------------------------------------ */ +/* 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); +} + +/* ------------------------------------------------------------------ */ /* users and tokens */ /* ------------------------------------------------------------------ */ @@ -1698,10 +1891,22 @@ static yyjson_mut_val *fy_json(struct req *r, sqlite3_stmt *st) sq(sqlite3_column_text(st, 5))); yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore", sqlite3_column_int64(st, 6)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "events", + sq(sqlite3_column_text(st, 7))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "agm_date", + sq(sqlite3_column_text(st, 8))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "dividend_date", + sq(sqlite3_column_text(st, 9))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "employees", + sq(sqlite3_column_text(st, 10))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes", + sq(sqlite3_column_text(st, 11))); return o; } -#define FY_COLUMNS "id,label,start_date,end_date,status,locked_until,dividend_ore" +#define FY_COLUMNS \ + "id,label,start_date,end_date,status,locked_until,dividend_ore," \ + "events,agm_date,dividend_date,employees,notes" static yyjson_mut_val *h_fiscal_year_list(struct req *r) { @@ -1779,20 +1984,46 @@ static yyjson_mut_val *h_fiscal_year_open(struct req *r) return fail(r, "INVALID_ARGS", "start_date must be before end_date"); if (fy_exists_in_range(r->db, r->org_id, start, end)) return fail(r, "CONFLICT", "fiscal year overlaps an existing one"); + /* "Information om året" carries over from the latest earlier year, so + only what changes needs to be edited. Dates and the dividend are + year-specific and start empty. */ + char prev_events[600] = "", prev_emp[64] = "", prev_notes[600] = ""; + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2( + r->db, + "SELECT events,employees,notes FROM fiscal_years" + " WHERE org_id=?1 AND end_date < ?2" + " ORDER BY end_date DESC LIMIT 1", + -1, &st, NULL) == SQLITE_OK) { + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_text(st, 2, start, -1, SQLITE_TRANSIENT); + if (sqlite3_step(st) == SQLITE_ROW) { + snprintf(prev_events, sizeof prev_events, "%s", + (const char *)sqlite3_column_text(st, 0)); + snprintf(prev_emp, sizeof prev_emp, "%s", + (const char *)sqlite3_column_text(st, 1)); + snprintf(prev_notes, sizeof prev_notes, "%s", + (const char *)sqlite3_column_text(st, 2)); + } + sqlite3_finalize(st); + } char ts[32]; util_iso8601(util_now(), ts, sizeof ts); - sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2( r->db, "INSERT INTO fiscal_years(org_id,label,start_date,end_date," - "created_at) VALUES(?1,?2,?3,?4,?5)", + "events,employees,notes,created_at)" + " VALUES(?1,?2,?3,?4,?5,?6,?7,?8)", -1, &st, NULL) != SQLITE_OK) return fail(r, "INTERNAL", "database error"); sqlite3_bind_int64(st, 1, r->org_id); sqlite3_bind_text(st, 2, label, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, start, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 4, end, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(st, 5, ts, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 5, prev_events, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 6, prev_emp, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 7, prev_notes, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 8, ts, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) @@ -1880,17 +2111,31 @@ static yyjson_mut_val *h_fiscal_year_reopen(struct req *r) return yyjson_mut_obj(r->rdoc); } -/* Fiscal-year metadata that is not a posting: today only the board's - proposed dividend, kept for the årsredovisning draft. */ +/* Fiscal-year metadata that is not a posting: the board's proposed + dividend and the year's material events, kept for the årsredovisning + draft. Both fields are optional; at least one must be given. */ static yyjson_mut_val *h_fiscal_year_update(struct req *r) { int64_t id = 0, dividend = 0; if (!arg_int(r->args, "id", &id) || id <= 0) return fail(r, "INVALID_ARGS", "id is required"); - if (!arg_int(r->args, "dividend_ore", ÷nd)) - return fail(r, "INVALID_ARGS", "dividend_ore is required"); - if (dividend < 0) + int have_div = arg_int(r->args, "dividend_ore", ÷nd); + const char *events = arg_str(r->args, "events"); + const char *agm = arg_str(r->args, "agm_date"); + const char *pay = arg_str(r->args, "dividend_date"); + const char *employees = arg_str(r->args, "employees"); + const char *notes = arg_str(r->args, "notes"); + if (!have_div && !events && !agm && !pay && !employees && !notes) + return fail(r, "INVALID_ARGS", "nothing to update"); + if (have_div && dividend < 0) return fail(r, "INVALID_ARGS", "dividend_ore must be >= 0"); + if (agm && *agm && !util_parse_iso_date(agm)) + return fail(r, "INVALID_ARGS", "agm_date must be YYYY-MM-DD"); + if (pay && *pay && !util_parse_iso_date(pay)) + return fail(r, "INVALID_ARGS", "dividend_date must be YYYY-MM-DD"); + static const char *const skeys[4] = { "events", "agm_date", + "dividend_date", "employees" }; + const char *svals[4] = { events, agm, pay, employees }; sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2(r->db, "SELECT count(*) FROM fiscal_years" @@ -1909,17 +2154,40 @@ static yyjson_mut_val *h_fiscal_year_update(struct req *r) 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); - yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore", dividend); + if (have_div) + yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore", dividend); + for (int i = 0; i < 4; i++) + if (svals[i]) + yyjson_mut_obj_add_strcpy(r->rdoc, o, skeys[i], svals[i]); + if (notes) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes", notes); return o; } if (sqlite3_prepare_v2(r->db, - "UPDATE fiscal_years SET dividend_ore=?3" + "UPDATE fiscal_years SET" + " dividend_ore=CASE WHEN ?3<0 THEN dividend_ore" + " ELSE ?3 END," + " events=COALESCE(?4,events)," + " agm_date=COALESCE(?5,agm_date)," + " dividend_date=COALESCE(?6,dividend_date)," + " employees=COALESCE(?7,employees)," + " notes=COALESCE(?8,notes)" " 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); - sqlite3_bind_int64(st, 3, dividend); + sqlite3_bind_int64(st, 3, have_div ? dividend : -1); + for (int i = 0; i < 4; i++) { + if (svals[i]) + sqlite3_bind_text(st, i + 4, svals[i], -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, i + 4); + } + if (notes) + sqlite3_bind_text(st, 8, notes, -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(st, 8); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) @@ -4148,6 +4416,12 @@ const struct command g_commands[] = { h_org_member_set_role }, { "org.member_remove", "Remove a member from the org", PERM_OWNER, 1, 1, 1, h_org_member_remove }, + { "board.list", "List board members", PERM_READ, 1, 0, 0, h_board_list }, + { "board.add", "Add a board member", PERM_OWNER, 1, 1, 1, h_board_add }, + { "board.update", "Update a board member", PERM_OWNER, 1, 1, 1, + h_board_update }, + { "board.remove", "Remove a board member", PERM_OWNER, 1, 1, 1, + h_board_remove }, { "user.create", "Create a user (system admin)", PERM_ADMIN, 0, 1, 1, h_user_create }, { "user.list", "List users (system admin)", PERM_ADMIN, 0, 0, 0, @@ -38,6 +38,8 @@ static const char SCHEMA_V1[] = " country TEXT NOT NULL DEFAULT 'SE'," " email TEXT," " phone TEXT," + " description TEXT NOT NULL DEFAULT ''," + " shares INTEGER NOT NULL DEFAULT 0," " fiscal_year_start_month INTEGER NOT NULL DEFAULT 1" " CHECK (fiscal_year_start_month BETWEEN 1 AND 12)," " moms_period TEXT NOT NULL DEFAULT 'month'" @@ -49,6 +51,15 @@ static const char SCHEMA_V1[] = " archived_at TEXT" ") STRICT;\n" + "CREATE TABLE board_members (" + " org_id INTEGER NOT NULL REFERENCES orgs(id)," + " id INTEGER PRIMARY KEY," + " name TEXT NOT NULL," + " title TEXT NOT NULL," + " created_at TEXT NOT NULL," + " UNIQUE (org_id, id)" + ") STRICT;\n" + "CREATE TABLE memberships (" " org_id INTEGER NOT NULL REFERENCES orgs(id)," " user_id INTEGER NOT NULL REFERENCES users(id)," @@ -97,6 +108,11 @@ static const char SCHEMA_V1[] = " status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','closed'))," " locked_until TEXT," " dividend_ore INTEGER NOT NULL DEFAULT 0," + " events TEXT NOT NULL DEFAULT ''," + " agm_date TEXT NOT NULL DEFAULT ''," + " dividend_date TEXT NOT NULL DEFAULT ''," + " employees TEXT NOT NULL DEFAULT ''," + " notes TEXT NOT NULL DEFAULT ''," " created_at TEXT NOT NULL," " closed_at TEXT," " closed_by INTEGER REFERENCES users(id)," @@ -372,6 +388,47 @@ static int db_upgrade_v4(sqlite3 *db, char **err) err); } +/* v5: årsredovisning details — the company description and share count, + the year's material events, and a board-member list for signatures. */ +static int db_upgrade_v5(sqlite3 *db, char **err) +{ + if (db_exec(db, "ALTER TABLE orgs ADD COLUMN description TEXT NOT NULL" + " DEFAULT ''", err) != 0) + return -1; + if (db_exec(db, "ALTER TABLE orgs ADD COLUMN shares INTEGER NOT NULL" + " DEFAULT 0", err) != 0) + return -1; + if (db_exec(db, "ALTER TABLE fiscal_years ADD COLUMN events TEXT NOT NULL" + " DEFAULT ''", err) != 0) + return -1; + return db_exec(db, + "CREATE TABLE IF NOT EXISTS board_members (" + " org_id INTEGER NOT NULL REFERENCES orgs(id)," + " id INTEGER PRIMARY KEY," + " name TEXT NOT NULL," + " title TEXT NOT NULL," + " created_at TEXT NOT NULL," + " UNIQUE (org_id, id)" + ") STRICT", + err); +} + +/* v6: "Information om året" — the year-specific årsredovisning details. */ +static int db_upgrade_v6(sqlite3 *db, char **err) +{ + if (db_exec(db, "ALTER TABLE fiscal_years ADD COLUMN agm_date TEXT NOT NULL" + " DEFAULT ''", err) != 0) + return -1; + if (db_exec(db, "ALTER TABLE fiscal_years ADD COLUMN dividend_date TEXT" + " NOT NULL DEFAULT ''", err) != 0) + return -1; + if (db_exec(db, "ALTER TABLE fiscal_years ADD COLUMN employees TEXT NOT NULL" + " DEFAULT ''", err) != 0) + return -1; + return db_exec(db, "ALTER TABLE fiscal_years ADD COLUMN notes TEXT NOT NULL" + " DEFAULT ''", err); +} + static int db_upgrade(sqlite3 *db, int from, char **err) { if (db_exec(db, "BEGIN IMMEDIATE", err) != 0) @@ -388,6 +445,14 @@ static int db_upgrade(sqlite3 *db, int from, char **err) db_exec(db, "ROLLBACK", NULL); return -1; } + if (from < 5 && db_upgrade_v5(db, err) != 0) { + db_exec(db, "ROLLBACK", NULL); + return -1; + } + if (from < 6 && db_upgrade_v6(db, err) != 0) { + db_exec(db, "ROLLBACK", NULL); + return -1; + } char *sql = sqlite3_mprintf( "UPDATE meta SET value='%d' WHERE key='schema_version'", BOKF_SCHEMA_VERSION); @@ -4,7 +4,7 @@ #include <sqlite3.h> #include <stdint.h> -#define BOKF_SCHEMA_VERSION 4 +#define BOKF_SCHEMA_VERSION 6 int db_open(const char *path, sqlite3 **out, char **err); int db_migrate(sqlite3 *db, char **err); |
