Files
company-tool/scripts/api-onboarding-test.mjs
Torsten Schulz (local) 0e539710c0 feat: Add password reset functionality with request and reset forms
feat: Implement price list import feature with preview and apply options

feat: Create price rules management page with CRUD operations

feat: Develop quotes management page with itemized quotes and status tracking

feat: Introduce organization registration page for new users

feat: Build suppliers management page with detailed supplier information

feat: Create users management page for inviting and managing roles

chore: Add TypeScript configuration for improved type checking

chore: Set up Vite configuration for development server and API proxy

chore: Add Vite environment type definitions for better TypeScript support
2026-06-02 15:28:38 +02:00

496 lines
23 KiB
JavaScript

#!/usr/bin/env node
const baseUrl = process.argv[2] ?? process.env.API_BASE_URL ?? "http://127.0.0.1:8080";
const email = `admin+${Date.now()}@example.com`;
async function request(method, path, body, token, options = {}) {
const response = await fetch(`${baseUrl}${path}`, {
method,
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
const data = text ? parseResponseBody(text) : {};
const expectedStatus = options.expectedStatus;
if (expectedStatus !== undefined) {
if (response.status !== expectedStatus) {
throw new Error(`${method} ${path} expected ${expectedStatus}, got ${response.status}: ${JSON.stringify(data)}`);
}
return data;
}
if (!response.ok) {
throw new Error(`${method} ${path} failed: ${response.status} ${JSON.stringify(data)}`);
}
return data;
}
function parseResponseBody(text) {
try {
return JSON.parse(text);
} catch {
return { message: text };
}
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
async function main() {
console.log(`testing onboarding api via ${baseUrl}`);
const registration = await request("POST", "/api/v1/registration/organization", {
organization_name: "Muster GmbH",
email,
accept_terms: true,
});
assert(registration.id, "registration id missing");
console.log(`registered ${registration.id}`);
const registrations = await request("GET", "/api/v1/admin/organization-registrations");
assert(
registrations.some((item) => item.id === registration.id),
"registration not found in list",
);
const detail = await request(
"GET",
`/api/v1/admin/organization-registrations/${registration.id}`,
);
assert(detail.organization_name === "Muster GmbH", "organization name did not decrypt");
assert(detail.email === email, "registration email mismatch");
const approval = await request(
"POST",
`/api/v1/admin/organization-registrations/${registration.id}/approve`,
);
assert(approval.organization_id, "organization id missing");
assert(approval.schema_name?.startsWith("company_"), "schema name missing");
assert(approval.dev_initial_password, "dev initial password missing");
console.log(`approved ${approval.organization_id}`);
const login = await request("POST", "/api/v1/auth/login", {
email,
password: approval.dev_initial_password,
});
assert(login.must_change_password === true, "must_change_password should be true");
assert(login.access_token, "access token missing");
assert(login.organization_id, "selected organization missing");
assert(login.organizations.length >= 1, "login organizations missing");
const token = login.access_token;
const selected = await request(
"POST",
"/api/v1/auth/select-organization",
{ organization_id: login.organization_id },
token,
);
assert(selected.selected === true, "organization was not selected");
const users = await request("GET", "/api/v1/organizations/current/users", undefined, token);
assert(users.length >= 1, "users list is empty");
assert(users.some((user) => user.email === email), "owner user missing");
const setup = await request("PUT", "/api/v1/organizations/current/setup", {
display_name: "Muster GmbH",
legal_form: "GmbH",
street: "Musterstrasse 1",
postal_code: "12345",
city: "Musterstadt",
country: "Deutschland",
vat_id: "",
email: "info@example.com",
phone: "",
default_tax_rate: "19",
default_payment_days: "14",
}, token);
assert(setup.saved === true, "organization setup was not saved");
const loadedSetup = await request("GET", "/api/v1/organizations/current/setup", undefined, token);
assert(loadedSetup.setup?.display_name === "Muster GmbH", "organization setup was not loaded");
assert(loadedSetup.setup?.street === "Musterstrasse 1", "organization setup street mismatch");
const numberRanges = await request("GET", "/api/v1/number-ranges", undefined, token);
assert(numberRanges.some((range) => range.code === "customers" && range.pattern === "KU{counter}"), "customer number range missing");
assert(numberRanges.some((range) => range.code === "items" && range.pattern === "AR{counter}"), "item number range missing");
assert(numberRanges.some((range) => range.code === "activities" && range.pattern === "AK{counter}"), "activity number range missing");
assert(numberRanges.some((range) => range.code === "outgoing_invoices" && range.pattern === "AR{counter}"), "invoice number range missing");
const cashDiscountTerm = await request("POST", "/api/v1/cash-discount-terms", {
code: "2-10-30",
name: "2 % Skonto bei Zahlung innerhalb von 10 Tagen",
discount_percent: "2.00",
discount_days: 10,
net_days: 30,
valid_from: null,
valid_until: null,
is_default_customer_term: true,
is_default_supplier_term: true,
is_active: true,
}, token);
assert(cashDiscountTerm.id, "cash discount term id missing");
const cashDiscountTerms = await request("GET", "/api/v1/cash-discount-terms", undefined, token);
assert(cashDiscountTerms.some((term) => term.id === cashDiscountTerm.id), "cash discount term missing");
const createdCustomer = await request("POST", "/api/v1/customers", {
customer_number: "",
name: "Beispielkunde GmbH",
status: "active",
details: {
street: "Kundenstraße 4",
postal_code: "54321",
city: "Kundenstadt",
country: "Deutschland",
email: "kunde@example.com",
phone: "01234 56789",
},
standard_discount_percent: "5.50",
cash_discount_term_id: cashDiscountTerm.id,
}, token);
assert(createdCustomer.id, "customer id missing");
assert(/^KU\d{3}\.\d{3}\.\d{3}$/.test(createdCustomer.customer_number), "customer number was not generated");
const customers = await request("GET", "/api/v1/customers", undefined, token);
const listedCustomer = customers.find((customer) => customer.id === createdCustomer.id);
assert(listedCustomer?.name === "Beispielkunde GmbH", "customer name was not loaded");
assert(listedCustomer?.details.city === "Kundenstadt", "customer details were not loaded");
assert(listedCustomer?.standard_discount_percent.startsWith("5.5"), "customer discount missing");
assert(listedCustomer?.cash_discount_term_id === cashDiscountTerm.id, "customer cash discount missing");
const updatedCustomer = await request("PUT", `/api/v1/customers/${createdCustomer.id}`, {
...createdCustomer,
name: "Beispielkunde AG",
standard_discount_percent: "7.00",
}, token);
assert(updatedCustomer.name === "Beispielkunde AG", "customer update failed");
const supplier = await request("POST", "/api/v1/suppliers", {
supplier_number: "",
name: "Beispiellieferant GmbH",
status: "active",
details: { street: "Lieferweg 1", postal_code: "10115", city: "Berlin", country: "Deutschland", email: "lieferant@example.com", phone: "" },
standard_discount_percent: "2.00",
cash_discount_term_id: cashDiscountTerm.id,
payment_days: 30,
}, token);
assert(/^LI\d{3}\.\d{3}\.\d{3}$/.test(supplier.supplier_number), "supplier number was not generated");
const suppliers = await request("GET", "/api/v1/suppliers", undefined, token);
assert(suppliers.some((record) => record.id === supplier.id && record.details.city === "Berlin"), "supplier CRUD failed");
assert(suppliers.some((record) => record.id === supplier.id && record.cash_discount_term_id === cashDiscountTerm.id), "supplier cash discount missing");
const item = await request("POST", "/api/v1/items", {
item_number: "",
name: "Montagestunde",
unit: "Std",
tax_rate: "19",
default_purchase_price: "40.00",
default_sales_price: "85.00",
status: "active",
}, token);
assert(/^AR\d{3}\.\d{3}\.\d{3}$/.test(item.item_number), "item number was not generated");
const updatedItem = await request("PUT", `/api/v1/items/${item.id}`, { ...item, default_sales_price: "95.00" }, token);
assert(updatedItem.default_sales_price === "95.00", "item update failed");
const priceHistory = await request("GET", `/api/v1/items/${item.id}/prices`, undefined, token);
assert(priceHistory.length >= 2, "item price history missing");
assert(priceHistory.some((entry) => entry.sales_price?.startsWith("95")), "updated item price history missing");
const priceListContent = [
"item_number;name;unit;tax_rate;purchase_price;sales_price",
"IMP-100;Importartikel;Stk;19;10.00;25.00",
`${item.item_number};Montagestunde Import;Std;19;42.00;99.00`,
].join("\n");
const importPreview = await request("POST", "/api/v1/imports/price-list/preview", {
source_name: "api-test-price-list.csv",
delimiter: ";",
content: priceListContent,
}, token);
assert(importPreview.total_rows === 2, "price import preview row count mismatch");
assert(importPreview.valid_rows === 2, "price import preview valid rows mismatch");
assert(importPreview.rows.some((row) => row.item_number === "IMP-100" && row.action === "create"), "price import create action missing");
assert(importPreview.rows.some((row) => row.item_number === item.item_number && row.action === "update"), "price import update action missing");
const importApply = await request("POST", "/api/v1/imports/price-list/apply", {
source_name: "api-test-price-list.csv",
delimiter: ";",
content: priceListContent,
}, token);
assert(importApply.import_id, "price import id missing");
assert(importApply.applied_rows === 2, "price import applied rows mismatch");
assert(importApply.error_rows === 0, "price import errors mismatch");
const importedItems = await request("GET", "/api/v1/items", undefined, token);
assert(importedItems.some((record) => record.item_number === "IMP-100" && record.name === "Importartikel"), "imported item missing");
const reloadedItem = importedItems.find((record) => record.id === item.id);
assert(reloadedItem?.default_sales_price?.startsWith("99"), "imported item price update missing");
const importedPriceHistory = await request("GET", `/api/v1/items/${item.id}/prices`, undefined, token);
assert(importedPriceHistory.some((entry) => entry.source.startsWith("import:") && entry.sales_price?.startsWith("99")), "import price history missing");
const connector = await request("POST", "/api/v1/api-connectors", {
code: "demo_price_api",
name: "Demo Preis API",
connector_type: "demo",
config: {
base_url: "https://example.test/api",
token: "secret",
delimiter: ";",
price_list_csv: [
"item_number;name;unit;tax_rate;purchase_price;sales_price",
"IMP-100;Importartikel API;Stk;19;11.00;27.00",
].join("\n"),
},
is_active: true,
sync_interval_minutes: 60,
}, token);
assert(connector.id, "api connector id missing");
assert(connector.config?.token === "secret", "api connector config roundtrip failed");
const connectors = await request("GET", "/api/v1/api-connectors", undefined, token);
assert(connectors.some((record) => record.id === connector.id && record.config?.base_url === "https://example.test/api"), "api connector list missing");
const connectorSync = await request("POST", `/api/v1/api-connectors/${connector.id}/sync`, {}, token);
assert(connectorSync.synced === true, "api connector sync failed");
assert(connectorSync.applied_rows === 1, "api connector price sync did not apply rows");
const syncedItems = await request("GET", "/api/v1/items", undefined, token);
assert(syncedItems.some((record) => record.item_number === "IMP-100" && record.default_sales_price?.startsWith("27")), "api connector price update missing");
const deletedConnector = await request("DELETE", `/api/v1/api-connectors/${connector.id}`, undefined, token);
assert(deletedConnector.deleted === true, "api connector deactivate failed");
const priceRule = await request("POST", "/api/v1/price-rules", {
code: "standard_import_markup",
name: "Standardaufschlag Import",
source_type: "import",
source_id: null,
markup_percent: "25.00",
rounding_mode: "cent",
is_active: true,
}, token);
assert(priceRule.id, "price rule id missing");
const priceRules = await request("GET", "/api/v1/price-rules", undefined, token);
assert(priceRules.some((record) => record.id === priceRule.id && record.markup_percent.startsWith("25")), "price rule list missing");
const updatedPriceRule = await request("PUT", `/api/v1/price-rules/${priceRule.id}`, {
...priceRule,
markup_percent: "30.00",
rounding_mode: "five_cent",
}, token);
assert(updatedPriceRule.rounding_mode === "five_cent", "price rule update failed");
const deletedPriceRule = await request("DELETE", `/api/v1/price-rules/${priceRule.id}`, undefined, token);
assert(deletedPriceRule.deleted === true, "price rule deactivate failed");
const quote = await request("POST", "/api/v1/quotes", {
quote_number: "",
customer_id: createdCustomer.id,
status: "draft",
valid_until: null,
cash_discount_term_id: cashDiscountTerm.id,
customer_discount_percent: "7.00",
notes: "Erstes Testangebot.",
items: [{
item_id: item.id,
description: "Montagestunde mit Sonderpreis",
quantity: "2.00",
unit_price: "90.00",
original_unit_price: "95.00",
discount_percent: "0.00",
tax_rate: "19.00",
}],
}, token);
assert(/^AN\d{3}\.\d{3}\.\d{3}$/.test(quote.quote_number), "quote number was not generated");
assert(quote.items.length === 1, "quote item missing");
assert(quote.items[0].price_overridden === true, "quote price override missing");
const quotes = await request("GET", "/api/v1/quotes", undefined, token);
assert(quotes.some((record) => record.id === quote.id && record.notes.includes("Testangebot")), "quote was not loaded");
const updatedQuote = await request("PUT", `/api/v1/quotes/${quote.id}`, {
...quote,
status: "sent",
items: quote.items.map((line) => ({ ...line, quantity: "3.00" })),
}, token);
assert(updatedQuote.status === "sent", "quote update failed");
const convertedInvoice = await request("POST", `/api/v1/quotes/${quote.id}/convert-to-invoice`, undefined, token);
assert(/^AR\d{3}\.\d{3}\.\d{3}$/.test(convertedInvoice.invoice_number), "converted invoice number missing");
assert(convertedInvoice.source_quote_id === quote.id, "converted invoice quote link missing");
assert(convertedInvoice.items.length === 1, "converted invoice item missing");
const invoices = await request("GET", "/api/v1/outgoing-invoices", undefined, token);
assert(invoices.some((record) => record.id === convertedInvoice.id), "outgoing invoice was not loaded");
const finalized = await request("POST", `/api/v1/outgoing-invoices/${convertedInvoice.id}/finalize`, undefined, token);
assert(finalized.finalized === true, "outgoing invoice finalize failed");
const deletedQuote = await request("DELETE", `/api/v1/quotes/${quote.id}`, undefined, token);
assert(deletedQuote.deleted === true, "quote cancel failed");
const incomingInvoice = await request("POST", "/api/v1/incoming-invoices", {
invoice_number: "EXT-10001",
supplier_id: supplier.id,
status: "received",
cash_discount_term_id: cashDiscountTerm.id,
invoice_date: null,
due_at: null,
items: [{
item_id: item.id,
description: "Einkauf Montagestunde",
quantity: "1.00",
unit_price: "40.00",
tax_rate: "19.00",
}],
}, token);
assert(incomingInvoice.id, "incoming invoice id missing");
assert(incomingInvoice.cash_discount_term_id === cashDiscountTerm.id, "incoming invoice cash discount missing");
const incomingInvoices = await request("GET", "/api/v1/incoming-invoices", undefined, token);
assert(incomingInvoices.some((record) => record.id === incomingInvoice.id), "incoming invoice was not loaded");
const deletedIncomingInvoice = await request("DELETE", `/api/v1/incoming-invoices/${incomingInvoice.id}`, undefined, token);
assert(deletedIncomingInvoice.deleted === true, "incoming invoice cancel failed");
const deletedItem = await request("DELETE", `/api/v1/items/${item.id}`, undefined, token);
assert(deletedItem.deleted === true, "item deactivate failed");
const deletedSupplier = await request("DELETE", `/api/v1/suppliers/${supplier.id}`, undefined, token);
assert(deletedSupplier.deleted === true, "supplier deactivate failed");
const deletedCustomer = await request("DELETE", `/api/v1/customers/${createdCustomer.id}`, undefined, token);
assert(deletedCustomer.deleted === true, "customer deactivate failed");
const activity = await request("POST", "/api/v1/activities", {
activity_number: null,
activity_type: "task",
title: "Angebot prüfen",
body: "Preise mit Kunde abstimmen.",
status: "open",
priority: "high",
due_at: null,
}, token);
assert(/^AK\d{3}\.\d{3}\.\d{3}$/.test(activity.activity_number), "activity number was not generated");
const activities = await request("GET", "/api/v1/activities", undefined, token);
assert(activities.some((record) => record.id === activity.id && record.body.includes("Preise")), "activity CRUD failed");
const deletedActivity = await request("DELETE", `/api/v1/activities/${activity.id}`, undefined, token);
assert(deletedActivity.deleted === true, "activity cancel failed");
const communication = await request("POST", "/api/v1/communications", {
communication_type: "email",
direction: "outbound",
subject: "Rückfrage zum Angebot",
body: "Kunde bittet um aktualisierte Dokumente.",
status: "open",
occurred_at: null,
links: [{ entity_type: "customer", entity_id: createdCustomer.id }],
}, token);
assert(communication.id, "communication id missing");
assert(communication.subject === "Rückfrage zum Angebot", "communication subject mismatch");
assert(communication.links.some((link) => link.entity_id === createdCustomer.id), "communication link missing");
const communications = await request("GET", "/api/v1/communications", undefined, token);
assert(communications.some((record) => record.id === communication.id), "communication list missing");
const documentContent = Buffer.from("Dokumentinhalt für Phase 6", "utf8").toString("base64");
const documentRecord = await request("POST", "/api/v1/documents", {
title: "Testdokument",
description: "Dokument für Phase 6",
file_name: "phase-6.txt",
content_type: "text/plain",
content_base64: documentContent,
links: [{ entity_type: "communication", entity_id: communication.id }],
}, token);
assert(documentRecord.id, "document id missing");
assert(documentRecord.latest_version?.file_name === "phase-6.txt", "document metadata missing");
const documents = await request("GET", "/api/v1/documents", undefined, token);
assert(documents.some((record) => record.id === documentRecord.id), "document list missing");
const downloadedDocument = await request("GET", `/api/v1/documents/${documentRecord.id}/download`, undefined, token);
assert(downloadedDocument.content_base64 === documentContent, "document download content mismatch");
const auditLog = await request("GET", `/api/v1/documents/${documentRecord.id}/audit-log`, undefined, token);
assert(auditLog.some((entry) => entry.action === "upload"), "document upload audit missing");
assert(auditLog.some((entry) => entry.action === "download"), "document download audit missing");
const deletedDocument = await request("DELETE", `/api/v1/documents/${documentRecord.id}`, undefined, token);
assert(deletedDocument.deleted === true, "document archive failed");
const invitedEmail = `user+${Date.now()}@example.com`;
const invitation = await request("POST", "/api/v1/organizations/current/invitations", {
email: invitedEmail,
roles: ["viewer"],
}, token);
assert(invitation.id, "invitation id missing");
assert(invitation.dev_invitation_token, "dev invitation token missing");
const invitedUsers = await request("GET", "/api/v1/organizations/current/users", undefined, token);
const invitedUser = invitedUsers.find((user) => user.user_id === invitation.user_id);
assert(invitedUser, "invited user missing");
const acceptedInvitation = await request("POST", "/api/v1/auth/accept-invitation", {
token: invitation.dev_invitation_token,
new_password: "InvitePass123",
new_password_confirm: "InvitePass123",
});
assert(acceptedInvitation.accepted === true, "invitation accept failed");
const invitedLogin = await request("POST", "/api/v1/auth/login", {
email: invitedEmail,
password: "InvitePass123",
});
assert(invitedLogin.access_token, "invited user login failed");
assert(invitedLogin.organization_id, "invited user selected organization missing");
const invitedToken = invitedLogin.access_token;
await request(
"POST",
"/api/v1/auth/select-organization",
{ organization_id: invitedLogin.organization_id },
invitedToken,
);
const deniedCustomerWrite = await request("POST", "/api/v1/customers", {
customer_number: "",
name: "Nicht erlaubt GmbH",
status: "active",
details: {
street: "Sperrweg 1",
postal_code: "12345",
city: "Teststadt",
country: "Deutschland",
email: "nicht-erlaubt@example.test",
phone: "",
},
standard_discount_percent: "0",
cash_discount_term_id: null,
}, invitedToken, { expectedStatus: 403 });
assert(deniedCustomerWrite.message === "Berechtigung fehlt", "viewer customer write was not forbidden");
const deniedRoleWrite = await request(
"PATCH",
`/api/v1/organizations/current/users/${invitation.user_id}/roles`,
{ roles: ["admin"] },
invitedToken,
{ expectedStatus: 403 },
);
assert(deniedRoleWrite.message === "Berechtigung fehlt", "viewer role write was not forbidden");
await request(
"PATCH",
`/api/v1/organizations/current/users/${invitation.user_id}/roles`,
{ roles: ["sales", "viewer"] },
token,
);
const updatedUsers = await request("GET", "/api/v1/organizations/current/users", undefined, token);
const updatedUser = updatedUsers.find((user) => user.user_id === invitation.user_id);
assert(updatedUser.roles.includes("sales"), "role change was not saved");
const resetRequest = await request("POST", "/api/v1/auth/request-password-reset", { email });
assert(resetRequest.queued === true, "password reset request failed");
assert(resetRequest.dev_reset_token, "dev reset token missing");
const resetPassword = await request("POST", "/api/v1/auth/reset-password", {
token: resetRequest.dev_reset_token,
new_password: "ResetPass123",
new_password_confirm: "ResetPass123",
});
assert(resetPassword.changed === true, "password reset failed");
const resetLogin = await request("POST", "/api/v1/auth/login", {
email,
password: "ResetPass123",
});
assert(resetLogin.access_token, "login after password reset failed");
console.log("onboarding api test ok");
}
main().catch((error) => {
console.error(error);
process.exit(1);
});