aboutsummaryrefslogtreecommitdiff
path: root/src/cmd_customers.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 20:11:19 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 20:11:19 +0200
commit8351fa7752bb8341ef405a37ff7c73142715e981 (patch)
treedde603d313f4f26934f2ce20a22dfd9b69324cf0 /src/cmd_customers.c
parentbeafdbe0d64340387d619c38554976410129f05b (diff)
downloadbokf-8351fa7752bb8341ef405a37ff7c73142715e981.tar.gz
bokf-8351fa7752bb8341ef405a37ff7c73142715e981.zip
commands: split the command table by domain
Diffstat (limited to 'src/cmd_customers.c')
-rw-r--r--src/cmd_customers.c517
1 files changed, 517 insertions, 0 deletions
diff --git a/src/cmd_customers.c b/src/cmd_customers.c
new file mode 100644
index 0000000..a46cd32
--- /dev/null
+++ b/src/cmd_customers.c
@@ -0,0 +1,517 @@
+#include "commands.h"
+#include "cmd_util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "audit.h"
+#include "db.h"
+#include "util.h"
+
+/* ------------------------------------------------------------------ */
+/* customer register */
+/* ------------------------------------------------------------------ */
+
+#define CUSTOMER_COLUMNS \
+ "id,name,address,postal_code,city,country,vat_nr,email,your_ref," \
+ "payment_days,notes,active,created_at,COALESCE(updated_at,'')"
+
+static yyjson_mut_val *customer_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, "address",
+ sq(sqlite3_column_text(st, 2)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "postal_code",
+ sq(sqlite3_column_text(st, 3)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "city",
+ sq(sqlite3_column_text(st, 4)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "country",
+ sq(sqlite3_column_text(st, 5)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "vat_nr",
+ sq(sqlite3_column_text(st, 6)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "email",
+ sq(sqlite3_column_text(st, 7)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "your_ref",
+ sq(sqlite3_column_text(st, 8)));
+ yyjson_mut_obj_add_int(r->rdoc, o, "payment_days",
+ sqlite3_column_int64(st, 9));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes",
+ sq(sqlite3_column_text(st, 10)));
+ yyjson_mut_obj_add_bool(r->rdoc, o, "active",
+ sqlite3_column_int(st, 11) != 0);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at",
+ sq(sqlite3_column_text(st, 12)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "updated_at",
+ sq(sqlite3_column_text(st, 13)));
+ return o;
+}
+
+struct customer_input {
+ const char *name;
+ const char *address;
+ const char *postal_code;
+ const char *city;
+ const char *country;
+ const char *vat_nr;
+ const char *email;
+ const char *your_ref;
+ const char *notes;
+ int64_t payment_days;
+ int have_payment;
+ int active;
+ int have_active;
+};
+
+static void customer_input_read(struct req *r, struct customer_input *in)
+{
+ memset(in, 0, sizeof *in);
+ in->name = arg_str(r->args, "name");
+ in->address = arg_str(r->args, "address");
+ in->postal_code = arg_str(r->args, "postal_code");
+ in->city = arg_str(r->args, "city");
+ in->country = arg_str(r->args, "country");
+ in->vat_nr = arg_str(r->args, "vat_nr");
+ in->email = arg_str(r->args, "email");
+ in->your_ref = arg_str(r->args, "your_ref");
+ in->notes = arg_str(r->args, "notes");
+ in->have_payment = arg_int(r->args, "payment_days", &in->payment_days);
+ in->have_active = arg_bool(r->args, "active", &in->active);
+}
+
+static int customer_input_validate(struct req *r,
+ const struct customer_input *in,
+ int is_create)
+{
+ if (is_create && (!in->name || !*in->name)) {
+ fail(r, "INVALID_ARGS", "name is required");
+ return -1;
+ }
+ if (in->name && !*in->name) {
+ fail(r, "INVALID_ARGS", "name cannot be empty");
+ return -1;
+ }
+ if (in->name && strlen(in->name) > 200) {
+ fail(r, "INVALID_ARGS", "name is too long");
+ return -1;
+ }
+ static const char *const names[] = {
+ "address", "postal_code", "city", "country", "vat_nr",
+ "email", "your_ref", "notes",
+ };
+ static const size_t maxlen[] = { 500, 32, 120, 64, 64, 254, 120, 2000 };
+ const char *values[] = { in->address, in->postal_code, in->city,
+ in->country, in->vat_nr, in->email,
+ in->your_ref, in->notes };
+ for (size_t i = 0; i < sizeof maxlen / sizeof maxlen[0]; i++) {
+ if (values[i] && strlen(values[i]) > maxlen[i]) {
+ failf(r, "INVALID_ARGS", "%s is too long", names[i]);
+ return -1;
+ }
+ }
+ if (in->have_payment && in->payment_days < 0) {
+ fail(r, "INVALID_ARGS", "payment_days must be >= 0");
+ return -1;
+ }
+ return 0;
+}
+
+static yyjson_mut_val *customer_lookup(struct req *r, int64_t id, int *found)
+{
+ sqlite3_stmt *st = NULL;
+ *found = 0;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT " CUSTOMER_COLUMNS " FROM customers"
+ " WHERE org_id=?1 AND id=?2",
+ -1, &st, NULL) != SQLITE_OK) {
+ *found = -1;
+ fail(r, "INTERNAL", "database error");
+ return NULL;
+ }
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, id);
+ if (sqlite3_step(st) != SQLITE_ROW) {
+ sqlite3_finalize(st);
+ return NULL;
+ }
+ yyjson_mut_val *o = customer_json(r, st);
+ sqlite3_finalize(st);
+ *found = 1;
+ return o;
+}
+
+static int customer_name_taken(struct req *r, const char *name, int64_t except_id)
+{
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT id FROM customers WHERE org_id=?1 AND name=?2"
+ " AND id<>?3",
+ -1, &st, NULL) != SQLITE_OK) {
+ fail(r, "INTERNAL", "database error");
+ return -1;
+ }
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int64(st, 3, except_id);
+ int taken = sqlite3_step(st) == SQLITE_ROW;
+ sqlite3_finalize(st);
+ return taken;
+}
+
+static yyjson_mut_val *h_customer_list(struct req *r)
+{
+ int active_only = 0;
+ arg_bool(r->args, "active_only", &active_only);
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT " CUSTOMER_COLUMNS " FROM customers WHERE org_id=?1"
+ " AND (?2=0 OR active=1) ORDER BY name COLLATE NOCASE, id",
+ -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int(st, 2, active_only);
+ yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
+ while (sqlite3_step(st) == SQLITE_ROW)
+ yyjson_mut_arr_add_val(items, customer_json(r, st));
+ 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_customer_get(struct req *r)
+{
+ int64_t id = 0;
+ if (!arg_int(r->args, "id", &id) || id <= 0)
+ return fail(r, "INVALID_ARGS", "id is required");
+ int found = 0;
+ yyjson_mut_val *o = customer_lookup(r, id, &found);
+ if (found < 0)
+ return NULL;
+ if (!found)
+ return fail(r, "NOT_FOUND", "customer not found");
+ return o;
+}
+
+static yyjson_mut_val *h_customer_create(struct req *r)
+{
+ struct customer_input in;
+ customer_input_read(r, &in);
+ if (customer_input_validate(r, &in, 1) != 0)
+ return NULL;
+ if (!in.have_payment)
+ in.payment_days = 30;
+ const char *address = in.address ? in.address : "";
+ const char *postal = in.postal_code ? in.postal_code : "";
+ const char *city = in.city ? in.city : "";
+ const char *country = in.country ? in.country : "SE";
+ const char *vat = in.vat_nr ? in.vat_nr : "";
+ const char *email = in.email ? in.email : "";
+ const char *your_ref = in.your_ref ? in.your_ref : "";
+ const char *notes = in.notes ? in.notes : "";
+ int taken = customer_name_taken(r, in.name, 0);
+ if (taken < 0)
+ return NULL;
+ if (taken)
+ return fail(r, "CONFLICT", "a customer with this name already exists");
+
+ if (r->dry_run) {
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", 0);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", in.name);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "address", address);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "postal_code", postal);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "city", city);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "country", country);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "vat_nr", vat);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "email", email);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "your_ref", your_ref);
+ yyjson_mut_obj_add_int(r->rdoc, o, "payment_days", in.payment_days);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes", notes);
+ yyjson_mut_obj_add_bool(r->rdoc, o, "active", true);
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ return o;
+ }
+
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "INSERT INTO customers(org_id,name,address,postal_code,city,country,"
+ "vat_nr,email,your_ref,payment_days,notes,created_at)"
+ " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
+ -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_text(st, 2, in.name, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 3, address, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 4, postal, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 5, city, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 6, country, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 7, vat, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 8, email, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 9, your_ref, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_int64(st, 10, in.payment_days);
+ sqlite3_bind_text(st, 11, notes, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 12, ts, -1, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ if ((rc & 0xff) == SQLITE_CONSTRAINT)
+ return fail(r, "CONFLICT", "a customer with this name already exists");
+ 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,
+ "customer.create", reqjson, "OK", NULL);
+ free(reqjson);
+ int found = 0;
+ yyjson_mut_val *o = customer_lookup(r, id, &found);
+ if (found < 0)
+ return NULL;
+ if (!found)
+ return fail(r, "INTERNAL", "could not read the new customer");
+ return o;
+}
+
+static yyjson_mut_val *h_customer_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");
+ struct customer_input in;
+ customer_input_read(r, &in);
+ if (customer_input_validate(r, &in, 0) != 0)
+ return NULL;
+ if (!in.name && !in.address && !in.postal_code && !in.city && !in.country &&
+ !in.vat_nr && !in.email && !in.your_ref && !in.notes &&
+ !in.have_payment && !in.have_active)
+ return fail(r, "INVALID_ARGS", "nothing to update");
+
+ int found = 0;
+ yyjson_mut_val *existing = customer_lookup(r, id, &found);
+ (void)existing;
+ if (found < 0)
+ return NULL;
+ if (!found)
+ return fail(r, "NOT_FOUND", "customer not found");
+ if (in.name) {
+ int taken = customer_name_taken(r, in.name, id);
+ if (taken < 0)
+ return NULL;
+ if (taken)
+ return fail(r, "CONFLICT", "a customer with this name already exists");
+ }
+
+ if (r->dry_run) {
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT id,COALESCE(?3,name),COALESCE(?4,address),"
+ "COALESCE(?5,postal_code),COALESCE(?6,city),"
+ "COALESCE(?7,country),COALESCE(?8,vat_nr),"
+ "COALESCE(?9,email),COALESCE(?10,your_ref),"
+ "CASE WHEN ?11<0 THEN payment_days ELSE ?11 END,"
+ "COALESCE(?12,notes),"
+ "CASE WHEN ?13<0 THEN active ELSE ?13 END,"
+ "created_at,COALESCE(updated_at,'')"
+ " FROM customers 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);
+ const char *texts[] = { in.name, in.address, in.postal_code, in.city,
+ in.country, in.vat_nr, in.email, in.your_ref,
+ in.notes };
+ static const int bind_index[] = { 3, 4, 5, 6, 7, 8, 9, 10, 12 };
+ for (size_t i = 0; i < sizeof texts / sizeof texts[0]; i++) {
+ if (texts[i])
+ sqlite3_bind_text(st, bind_index[i], texts[i], -1,
+ SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, bind_index[i]);
+ }
+ sqlite3_bind_int64(st, 11, in.have_payment ? in.payment_days : -1);
+ sqlite3_bind_int64(st, 13, in.have_active ? in.active : -1);
+ yyjson_mut_val *o = NULL;
+ if (sqlite3_step(st) == SQLITE_ROW)
+ o = customer_json(r, st);
+ sqlite3_finalize(st);
+ if (!o)
+ return fail(r, "NOT_FOUND", "customer not found");
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ return o;
+ }
+
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "UPDATE customers SET"
+ " name=COALESCE(?3,name), address=COALESCE(?4,address),"
+ " postal_code=COALESCE(?5,postal_code), city=COALESCE(?6,city),"
+ " country=COALESCE(?7,country), vat_nr=COALESCE(?8,vat_nr),"
+ " email=COALESCE(?9,email), your_ref=COALESCE(?10,your_ref),"
+ " payment_days=CASE WHEN ?11<0 THEN payment_days ELSE ?11 END,"
+ " notes=COALESCE(?12,notes),"
+ " active=CASE WHEN ?13<0 THEN active ELSE ?13 END,"
+ " updated_at=?14 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);
+ const char *texts[] = { in.name, in.address, in.postal_code, in.city,
+ in.country, in.vat_nr, in.email, in.your_ref,
+ in.notes };
+ static const int text_index[] = { 3, 4, 5, 6, 7, 8, 9, 10, 12 };
+ for (size_t i = 0; i < sizeof texts / sizeof texts[0]; i++) {
+ if (texts[i])
+ sqlite3_bind_text(st, text_index[i], texts[i], -1,
+ SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, text_index[i]);
+ }
+ sqlite3_bind_int64(st, 11, in.have_payment ? in.payment_days : -1);
+ sqlite3_bind_int64(st, 13, in.have_active ? in.active : -1);
+ sqlite3_bind_text(st, 14, ts, -1, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ if ((rc & 0xff) == SQLITE_CONSTRAINT)
+ return fail(r, "CONFLICT", "a customer with this name already exists");
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ }
+ if (sqlite3_changes(r->db) == 0)
+ return fail(r, "NOT_FOUND", "customer not found");
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "customer.update", reqjson, "OK", NULL);
+ free(reqjson);
+ found = 0;
+ yyjson_mut_val *o = customer_lookup(r, id, &found);
+ if (found < 0)
+ return NULL;
+ if (!found)
+ return fail(r, "INTERNAL", "could not read the customer");
+ return o;
+}
+
+static yyjson_mut_val *h_customer_archive(struct req *r)
+{
+ int64_t id = 0;
+ int active = 0;
+ if (!arg_int(r->args, "id", &id) || id <= 0 ||
+ !arg_bool(r->args, "active", &active))
+ return fail(r, "INVALID_ARGS", "id and active are required");
+ int found = 0;
+ yyjson_mut_val *existing = customer_lookup(r, id, &found);
+ (void)existing;
+ if (found < 0)
+ return NULL;
+ if (!found)
+ return fail(r, "NOT_FOUND", "customer not found");
+
+ if (r->dry_run) {
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
+ yyjson_mut_obj_add_bool(r->rdoc, o, "active", active != 0);
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ return o;
+ }
+
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "UPDATE customers SET active=?3, updated_at=?4"
+ " 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_int(st, 3, active);
+ 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));
+ if (sqlite3_changes(r->db) == 0)
+ return fail(r, "NOT_FOUND", "customer not found");
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "customer.archive", 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_bool(r->rdoc, o, "active", active != 0);
+ return o;
+}
+
+
+static const struct cmd_arg args_customer_list[] = {
+ { "active_only", ARG_BOOL, 0, NULL, NULL, "Only active customers" },
+};
+
+static const struct cmd_arg args_customer_get[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Customer id" },
+};
+
+static const struct cmd_arg args_customer_create[] = {
+ { "name", ARG_STR, 1, NULL, NULL, "Customer name, unique per org" },
+ { "address", ARG_STR, 0, NULL, NULL, "Street address; may contain newlines" },
+ { "postal_code", ARG_STR, 0, NULL, NULL, "Postal code" },
+ { "city", ARG_STR, 0, NULL, NULL, "City" },
+ { "country", ARG_STR, 0, "SE", NULL, "Country code" },
+ { "vat_nr", ARG_STR, 0, NULL, NULL, "VAT number" },
+ { "email", ARG_STR, 0, NULL, NULL, "E-mail address" },
+ { "your_ref", ARG_STR, 0, NULL, NULL, "Customer reference" },
+ { "notes", ARG_STR, 0, NULL, NULL, "Free-text notes" },
+ { "payment_days", ARG_INT, 0, "30", NULL, "Payment terms in days" },
+};
+
+static const struct cmd_arg args_customer_update[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Customer id" },
+ { "name", ARG_STR, 0, NULL, NULL, "Customer name, unique per org" },
+ { "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 code" },
+ { "vat_nr", ARG_STR, 0, NULL, NULL, "VAT number" },
+ { "email", ARG_STR, 0, NULL, NULL, "E-mail address" },
+ { "your_ref", ARG_STR, 0, NULL, NULL, "Customer reference" },
+ { "notes", ARG_STR, 0, NULL, NULL, "Free-text notes" },
+ { "payment_days", ARG_INT, 0, NULL, NULL, "Payment terms in days" },
+ { "active", ARG_BOOL, 0, NULL, NULL, "Active flag" },
+};
+
+static const struct cmd_arg args_customer_archive[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Customer id" },
+ { "active", ARG_BOOL, 1, NULL, NULL, "false archives, true reactivates" },
+};
+
+const struct command g_cmd_customers[] = {
+ { "customer.list", "List customers ordered by name", PERM_READ, 1, 0, 0,
+ h_customer_list, CMD_ARGS(args_customer_list) },
+ { "customer.get", "Get one customer", PERM_READ, 1, 0, 0, h_customer_get,
+ CMD_ARGS(args_customer_get) },
+ { "customer.create", "Create a customer", PERM_WRITE, 1, 1, 1,
+ h_customer_create, CMD_ARGS(args_customer_create) },
+ { "customer.update", "Update a customer (merged)", PERM_WRITE, 1, 1, 1,
+ h_customer_update, CMD_ARGS(args_customer_update) },
+ { "customer.archive", "Archive or reactivate a customer", PERM_WRITE, 1, 1,
+ 1, h_customer_archive, CMD_ARGS(args_customer_archive) },
+};
+
+const struct cmd_table g_cmd_table_customers = {
+ g_cmd_customers, sizeof g_cmd_customers / sizeof g_cmd_customers[0]
+};