From 4488b6c2eb1a3ddf20e190ebd28953e4f3f01042 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Mon, 21 Sep 2026 09:56:01 +0200 Subject: payroll: lönebesked PDF and mail (schema v11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cmd_employees.c | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) (limited to 'src/cmd_employees.c') diff --git a/src/cmd_employees.c b/src/cmd_employees.c index 788dd83..952bd2b 100644 --- a/src/cmd_employees.c +++ b/src/cmd_employees.c @@ -17,7 +17,7 @@ #define EMPLOYEE_COLUMNS \ "id,name,personal_no_enc,address,postal_code,city,bank_account," \ "salary_account,monthly_salary_ore,tax_table,tax_column,active," \ - "created_at,COALESCE(updated_at,'')" + "created_at,COALESCE(updated_at,''),email" static int personal_no_valid(const char *s) { @@ -90,6 +90,8 @@ static yyjson_mut_val *employee_json(struct req *r, sqlite3_stmt *st) sq(sqlite3_column_text(st, 12))); yyjson_mut_obj_add_strcpy(r->rdoc, o, "updated_at", sq(sqlite3_column_text(st, 13))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "email", + sq(sqlite3_column_text(st, 14))); return o; } @@ -129,6 +131,7 @@ struct employee_input { const char *city; const char *bank_account; const char *salary_account; + const char *email; int64_t monthly_salary_ore; int have_salary; int64_t tax_table; @@ -149,6 +152,7 @@ static void employee_input_read(struct req *r, struct employee_input *in) in->city = arg_str(r->args, "city"); in->bank_account = arg_str(r->args, "bank_account"); in->salary_account = arg_str(r->args, "salary_account"); + in->email = arg_str(r->args, "email"); in->have_salary = arg_int(r->args, "monthly_salary_ore", &in->monthly_salary_ore); in->have_table = arg_int(r->args, "tax_table", &in->tax_table); @@ -210,6 +214,21 @@ static int employee_input_validate(struct req *r, fail(r, "INVALID_ARGS", "tax_column must be between 1 and 6"); return -1; } + if (in->email) { + size_t len = strlen(in->email); + if (len > 254) { + fail(r, "INVALID_ARGS", "email is too long"); + return -1; + } + for (const char *p = in->email; *p; p++) { + unsigned char ch = (unsigned char)*p; + if (ch < 32 || ch == 127) { + fail(r, "INVALID_ARGS", + "email must not contain control characters"); + return -1; + } + } + } return 0; } @@ -346,6 +365,7 @@ static yyjson_mut_val *h_employee_create(struct req *r) const char *postal = in.postal_code ? in.postal_code : ""; const char *city = in.city ? in.city : ""; const char *bank = in.bank_account ? in.bank_account : ""; + const char *email = in.email ? in.email : ""; if (r->dry_run) { free(stored); @@ -365,6 +385,7 @@ static yyjson_mut_val *h_employee_create(struct req *r) yyjson_mut_obj_add_int(r->rdoc, o, "tax_table", in.tax_table); yyjson_mut_obj_add_int(r->rdoc, o, "tax_column", in.tax_column); yyjson_mut_obj_add_bool(r->rdoc, o, "active", true); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "email", email); yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true); return o; } @@ -376,8 +397,8 @@ static yyjson_mut_val *h_employee_create(struct req *r) r->db, "INSERT INTO employees(org_id,name,personal_no_enc,address," "postal_code,city,bank_account,salary_account,monthly_salary_ore," - "tax_table,tax_column,created_at)" - " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)", + "tax_table,tax_column,created_at,email)" + " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13)", -1, &st, NULL) != SQLITE_OK) { free(stored); return db_error(r); @@ -394,6 +415,7 @@ static yyjson_mut_val *h_employee_create(struct req *r) sqlite3_bind_int64(st, 10, in.tax_table); sqlite3_bind_int64(st, 11, in.tax_column); sqlite3_bind_text(st, 12, ts, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(st, 13, email, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); sqlite3_finalize(st); free(stored); @@ -427,7 +449,7 @@ static yyjson_mut_val *h_employee_update(struct req *r) if (employee_input_validate(r, &in, 0) != 0) return NULL; if (!in.name && !in.personal_no && !in.address && !in.postal_code && - !in.city && !in.bank_account && !in.salary_account && + !in.city && !in.bank_account && !in.salary_account && !in.email && !in.have_salary && !in.have_table && !in.have_column && !in.have_active) return fail(r, "INVALID_ARGS", "nothing to update"); @@ -457,7 +479,7 @@ static yyjson_mut_val *h_employee_update(struct req *r) "CASE WHEN ?11<0 THEN tax_table ELSE ?11 END," "CASE WHEN ?12<0 THEN tax_column ELSE ?12 END," "CASE WHEN ?13<0 THEN active ELSE ?13 END," - "created_at,COALESCE(updated_at,'')" + "created_at,COALESCE(updated_at,''),COALESCE(?14,email)" " FROM employees WHERE org_id=?1 AND id=?2", -1, &st, NULL) != SQLITE_OK) { free(stored); @@ -477,6 +499,7 @@ static yyjson_mut_val *h_employee_update(struct req *r) sqlite3_bind_int64(st, 11, in.have_table ? in.tax_table : -1); sqlite3_bind_int64(st, 12, in.have_column ? in.tax_column : -1); sqlite3_bind_int64(st, 13, in.have_active ? in.active : -1); + bind_text_or_null(st, 14, in.email); yyjson_mut_val *o = NULL; if (sqlite3_step(st) == SQLITE_ROW) o = employee_json(r, st); @@ -506,7 +529,8 @@ static yyjson_mut_val *h_employee_update(struct req *r) " tax_table=CASE WHEN ?11<0 THEN tax_table ELSE ?11 END," " tax_column=CASE WHEN ?12<0 THEN tax_column ELSE ?12 END," " active=CASE WHEN ?13<0 THEN active ELSE ?13 END," - " updated_at=?14 WHERE org_id=?1 AND id=?2", + " email=COALESCE(?14,email)," + " updated_at=?15 WHERE org_id=?1 AND id=?2", -1, &st, NULL) != SQLITE_OK) { free(stored); return db_error(r); @@ -524,7 +548,8 @@ static yyjson_mut_val *h_employee_update(struct req *r) sqlite3_bind_int64(st, 11, in.have_table ? in.tax_table : -1); sqlite3_bind_int64(st, 12, in.have_column ? in.tax_column : -1); sqlite3_bind_int64(st, 13, in.have_active ? in.active : -1); - sqlite3_bind_text(st, 14, ts, -1, SQLITE_TRANSIENT); + bind_text_or_null(st, 14, in.email); + sqlite3_bind_text(st, 15, ts, -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); sqlite3_finalize(st); free(stored); @@ -617,6 +642,7 @@ static const struct cmd_arg args_employee_create[] = { { "postal_code", ARG_STR, 0, NULL, NULL, "Postal code" }, { "city", ARG_STR, 0, NULL, NULL, "City" }, { "bank_account", ARG_STR, 0, NULL, NULL, "Bank account for the net pay" }, + { "email", ARG_STR, 0, NULL, NULL, "E-mail for lönebesked" }, { "salary_account", ARG_STR, 0, NULL, NULL, "Salary account, digits only; defaults to payroll_salary_account" }, { "monthly_salary_ore", ARG_INT, 0, "0", NULL, @@ -634,6 +660,7 @@ static const struct cmd_arg args_employee_update[] = { { "postal_code", ARG_STR, 0, NULL, NULL, "Postal code" }, { "city", ARG_STR, 0, NULL, NULL, "City" }, { "bank_account", ARG_STR, 0, NULL, NULL, "Bank account for the net pay" }, + { "email", ARG_STR, 0, NULL, NULL, "E-mail for lönebesked" }, { "salary_account", ARG_STR, 0, NULL, NULL, "Salary account, digits only" }, { "monthly_salary_ore", ARG_INT, 0, NULL, NULL, "Monthly gross salary in öre" }, -- cgit v1.3