From 5c03570f071554c1d7e761f1a22a6fcdccd28b95 Mon Sep 17 00:00:00 2001 From: BOHA Date: Tue, 9 Jun 2026 19:31:36 +0200 Subject: [PATCH] feat(settings): separate numbering config for received orders, issued orders, and projects Co-Authored-By: Claude Opus 4.8 (1M context) --- .../company-settings-numbering.test.ts | 132 ++++++++++++++++++ src/admin/lib/queries/settings.ts | 4 + src/admin/pages/CompanySettings.tsx | 31 +++- src/routes/admin/company-settings.ts | 18 ++- src/schemas/settings.schema.ts | 4 + 5 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/company-settings-numbering.test.ts diff --git a/src/__tests__/company-settings-numbering.test.ts b/src/__tests__/company-settings-numbering.test.ts new file mode 100644 index 0000000..ed00be6 --- /dev/null +++ b/src/__tests__/company-settings-numbering.test.ts @@ -0,0 +1,132 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import Fastify from "fastify"; +import jwt from "jsonwebtoken"; +import companySettingsRoutes from "../routes/admin/company-settings"; +import prisma from "../config/database"; +import { config } from "../config/env"; + +// Fixture prefix so cleanup never touches real data. +const N = "settings_numbering_test_"; + +let app: ReturnType; +let token: string; + +// Snapshot the prior values of the four numbering fields we mutate so we can +// restore them in afterAll — other tests read company_settings and must not be +// poisoned by our writes. +let priorValues: { + issued_order_number_pattern: string | null; + issued_order_type_code: string | null; + project_number_pattern: string | null; + project_type_code: string | null; +} | null = null; +let settingsId: number | null = null; + +beforeAll(async () => { + app = Fastify({ logger: false }); + await app.register(companySettingsRoutes, { + prefix: "/api/admin/company-settings", + }); + + // Clean up any leftover fixture from a previous failed run. + await prisma.users + .deleteMany({ where: { username: { startsWith: N } } }) + .catch(() => {}); + + // Attach the fixture user to the admin role so the PUT/GET permission guards + // (settings.company / settings.system) pass. + const adminRole = await prisma.roles.findFirst({ where: { name: "admin" } }); + if (!adminRole) + throw new Error("Test setup: no admin role found — seed the database"); + + const user = await prisma.users.create({ + data: { + username: `${N}user`, + email: `${N}user@test.local`, + password_hash: "x", + first_name: "Settings", + last_name: "Test", + is_active: true, + totp_enabled: false, + role_id: adminRole.id, + }, + }); + token = jwt.sign( + { sub: user.id, username: user.username, role: "admin" }, + config.jwt.secret, + { expiresIn: config.jwt.accessTokenExpiry }, + ); + + // Snapshot existing settings so we can restore after the suite. + const existing = await prisma.company_settings.findFirst({ + select: { + id: true, + issued_order_number_pattern: true, + issued_order_type_code: true, + project_number_pattern: true, + project_type_code: true, + }, + }); + if (existing) { + settingsId = existing.id; + priorValues = { + issued_order_number_pattern: existing.issued_order_number_pattern, + issued_order_type_code: existing.issued_order_type_code, + project_number_pattern: existing.project_number_pattern, + project_type_code: existing.project_type_code, + }; + } +}); + +afterAll(async () => { + // Restore the four numbering fields to their pre-test values. + if (settingsId !== null && priorValues !== null) { + await prisma.company_settings + .update({ where: { id: settingsId }, data: priorValues }) + .catch(() => {}); + } + await prisma.users + .deleteMany({ where: { username: { startsWith: N } } }) + .catch(() => {}); + await app.close(); +}); + +describe("company-settings numbering — issued orders + projects round-trip", () => { + it("persists and returns issued_order_* and project_* numbering fields", async () => { + const payload = { + issued_order_number_pattern: "{YY}V{NNNN}", + issued_order_type_code: "72", + project_number_pattern: "{YY}P{NNNN}", + project_type_code: "73", + }; + + const put = await app.inject({ + method: "PUT", + url: "/api/admin/company-settings/", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + expect(put.statusCode).toBe(200); + expect(put.json().success).toBe(true); + + const get = await app.inject({ + method: "GET", + url: "/api/admin/company-settings/", + headers: { authorization: `Bearer ${token}` }, + }); + expect(get.statusCode).toBe(200); + const data = get.json().data; + expect(data.issued_order_number_pattern).toBe("{YY}V{NNNN}"); + expect(data.issued_order_type_code).toBe("72"); + expect(data.project_number_pattern).toBe("{YY}P{NNNN}"); + expect(data.project_type_code).toBe("73"); + }); + + it("requires authentication", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/admin/company-settings/", + }); + expect(res.statusCode).toBe(401); + }); +}); diff --git a/src/admin/lib/queries/settings.ts b/src/admin/lib/queries/settings.ts index 2fc8399..76e11e3 100644 --- a/src/admin/lib/queries/settings.ts +++ b/src/admin/lib/queries/settings.ts @@ -48,6 +48,10 @@ export interface CompanySettingsData { offer_number_pattern?: string; order_number_pattern?: string; invoice_number_pattern?: string; + issued_order_number_pattern?: string; + issued_order_type_code?: string; + project_number_pattern?: string; + project_type_code?: string; warehouse_receipt_prefix?: string; warehouse_receipt_number_pattern?: string; warehouse_issue_prefix?: string; diff --git a/src/admin/pages/CompanySettings.tsx b/src/admin/pages/CompanySettings.tsx index 262e2bd..0433bd6 100644 --- a/src/admin/pages/CompanySettings.tsx +++ b/src/admin/pages/CompanySettings.tsx @@ -131,6 +131,10 @@ interface CompanyForm { offer_number_pattern: string; order_number_pattern: string; invoice_number_pattern: string; + issued_order_number_pattern: string; + issued_order_type_code: string; + project_number_pattern: string; + project_type_code: string; quotation_prefix: string; order_type_code: string; invoice_type_code: string; @@ -182,6 +186,10 @@ export default function CompanySettings({ offer_number_pattern: "", order_number_pattern: "", invoice_number_pattern: "", + issued_order_number_pattern: "", + issued_order_type_code: "", + project_number_pattern: "", + project_type_code: "", quotation_prefix: "", order_type_code: "", invoice_type_code: "", @@ -347,6 +355,11 @@ export default function CompanySettings({ offer_number_pattern: settingsData.offer_number_pattern || "", order_number_pattern: settingsData.order_number_pattern || "", invoice_number_pattern: settingsData.invoice_number_pattern || "", + issued_order_number_pattern: + settingsData.issued_order_number_pattern || "", + issued_order_type_code: settingsData.issued_order_type_code || "", + project_number_pattern: settingsData.project_number_pattern || "", + project_type_code: settingsData.project_type_code || "", quotation_prefix: settingsData.quotation_prefix || "", order_type_code: settingsData.order_type_code || "", invoice_type_code: settingsData.invoice_type_code || "", @@ -1147,13 +1160,29 @@ export default function CompanySettings({ codeKey: null, }, { - label: "Objednávky a projekty", + label: "Objednávky přijaté", patternKey: "order_number_pattern" as const, defaultPattern: "{YY}{CODE}{NNNN}", prefixKey: null, codeKey: "order_type_code" as const, codeLabel: "Typový kód", }, + { + label: "Objednávky vydané", + patternKey: "issued_order_number_pattern" as const, + defaultPattern: "{YY}{CODE}{NNNN}", + prefixKey: null, + codeKey: "issued_order_type_code" as const, + codeLabel: "Typový kód", + }, + { + label: "Projekty", + patternKey: "project_number_pattern" as const, + defaultPattern: "{YY}{CODE}{NNNN}", + prefixKey: null, + codeKey: "project_type_code" as const, + codeLabel: "Typový kód", + }, { label: "Faktury", patternKey: "invoice_number_pattern" as const, diff --git a/src/routes/admin/company-settings.ts b/src/routes/admin/company-settings.ts index 319c948..9c0c3b8 100644 --- a/src/routes/admin/company-settings.ts +++ b/src/routes/admin/company-settings.ts @@ -178,6 +178,10 @@ export default async function companySettingsRoutes( offer_number_pattern: true, order_number_pattern: true, invoice_number_pattern: true, + issued_order_number_pattern: true, + issued_order_type_code: true, + project_number_pattern: true, + project_type_code: true, warehouse_receipt_prefix: true, warehouse_receipt_number_pattern: true, warehouse_issue_prefix: true, @@ -234,6 +238,10 @@ export default async function companySettingsRoutes( offer_number_pattern: true, order_number_pattern: true, invoice_number_pattern: true, + issued_order_number_pattern: true, + issued_order_type_code: true, + project_number_pattern: true, + project_type_code: true, warehouse_receipt_prefix: true, warehouse_receipt_number_pattern: true, warehouse_issue_prefix: true, @@ -286,6 +294,10 @@ export default async function companySettingsRoutes( offer_number_pattern: true, order_number_pattern: true, invoice_number_pattern: true, + issued_order_number_pattern: true, + issued_order_type_code: true, + project_number_pattern: true, + project_type_code: true, warehouse_receipt_prefix: true, warehouse_receipt_number_pattern: true, warehouse_issue_prefix: true, @@ -310,7 +322,6 @@ export default async function companySettingsRoutes( settings.custom_fields as string | null, ); - const pkg = require("../../../package.json") as { version: string }; let available_vat_rates: number[] = [0, 10, 12, 15, 21]; @@ -354,7 +365,6 @@ export default async function companySettingsRoutes( "/system-info", { preHandler: requirePermission("settings.system") }, async (request, reply) => { - const pkg = require("../../../package.json") as { version: string }; const uptimeSec = process.uptime(); const days = Math.floor(uptimeSec / 86400); @@ -452,6 +462,10 @@ export default async function companySettingsRoutes( "offer_number_pattern", "order_number_pattern", "invoice_number_pattern", + "issued_order_number_pattern", + "issued_order_type_code", + "project_number_pattern", + "project_type_code", "warehouse_receipt_prefix", "warehouse_receipt_number_pattern", "warehouse_issue_prefix", diff --git a/src/schemas/settings.schema.ts b/src/schemas/settings.schema.ts index 1b94390..e1f26cb 100644 --- a/src/schemas/settings.schema.ts +++ b/src/schemas/settings.schema.ts @@ -32,6 +32,10 @@ export const UpdateCompanySettingsSchema = z.object({ offer_number_pattern: z.string().nullish(), order_number_pattern: z.string().nullish(), invoice_number_pattern: z.string().nullish(), + issued_order_number_pattern: z.string().nullish(), + issued_order_type_code: z.string().nullish(), + project_number_pattern: z.string().nullish(), + project_type_code: z.string().nullish(), warehouse_receipt_prefix: z.string().nullish(), warehouse_receipt_number_pattern: z.string().nullish(), warehouse_issue_prefix: z.string().nullish(),