feat(settings): separate numbering config for received orders, issued orders, and projects
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
132
src/__tests__/company-settings-numbering.test.ts
Normal file
132
src/__tests__/company-settings-numbering.test.ts
Normal file
@@ -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<typeof Fastify>;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -48,6 +48,10 @@ export interface CompanySettingsData {
|
|||||||
offer_number_pattern?: string;
|
offer_number_pattern?: string;
|
||||||
order_number_pattern?: string;
|
order_number_pattern?: string;
|
||||||
invoice_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_prefix?: string;
|
||||||
warehouse_receipt_number_pattern?: string;
|
warehouse_receipt_number_pattern?: string;
|
||||||
warehouse_issue_prefix?: string;
|
warehouse_issue_prefix?: string;
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ interface CompanyForm {
|
|||||||
offer_number_pattern: string;
|
offer_number_pattern: string;
|
||||||
order_number_pattern: string;
|
order_number_pattern: string;
|
||||||
invoice_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;
|
quotation_prefix: string;
|
||||||
order_type_code: string;
|
order_type_code: string;
|
||||||
invoice_type_code: string;
|
invoice_type_code: string;
|
||||||
@@ -182,6 +186,10 @@ export default function CompanySettings({
|
|||||||
offer_number_pattern: "",
|
offer_number_pattern: "",
|
||||||
order_number_pattern: "",
|
order_number_pattern: "",
|
||||||
invoice_number_pattern: "",
|
invoice_number_pattern: "",
|
||||||
|
issued_order_number_pattern: "",
|
||||||
|
issued_order_type_code: "",
|
||||||
|
project_number_pattern: "",
|
||||||
|
project_type_code: "",
|
||||||
quotation_prefix: "",
|
quotation_prefix: "",
|
||||||
order_type_code: "",
|
order_type_code: "",
|
||||||
invoice_type_code: "",
|
invoice_type_code: "",
|
||||||
@@ -347,6 +355,11 @@ export default function CompanySettings({
|
|||||||
offer_number_pattern: settingsData.offer_number_pattern || "",
|
offer_number_pattern: settingsData.offer_number_pattern || "",
|
||||||
order_number_pattern: settingsData.order_number_pattern || "",
|
order_number_pattern: settingsData.order_number_pattern || "",
|
||||||
invoice_number_pattern: settingsData.invoice_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 || "",
|
quotation_prefix: settingsData.quotation_prefix || "",
|
||||||
order_type_code: settingsData.order_type_code || "",
|
order_type_code: settingsData.order_type_code || "",
|
||||||
invoice_type_code: settingsData.invoice_type_code || "",
|
invoice_type_code: settingsData.invoice_type_code || "",
|
||||||
@@ -1147,13 +1160,29 @@ export default function CompanySettings({
|
|||||||
codeKey: null,
|
codeKey: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Objednávky a projekty",
|
label: "Objednávky přijaté",
|
||||||
patternKey: "order_number_pattern" as const,
|
patternKey: "order_number_pattern" as const,
|
||||||
defaultPattern: "{YY}{CODE}{NNNN}",
|
defaultPattern: "{YY}{CODE}{NNNN}",
|
||||||
prefixKey: null,
|
prefixKey: null,
|
||||||
codeKey: "order_type_code" as const,
|
codeKey: "order_type_code" as const,
|
||||||
codeLabel: "Typový kód",
|
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",
|
label: "Faktury",
|
||||||
patternKey: "invoice_number_pattern" as const,
|
patternKey: "invoice_number_pattern" as const,
|
||||||
|
|||||||
@@ -178,6 +178,10 @@ export default async function companySettingsRoutes(
|
|||||||
offer_number_pattern: true,
|
offer_number_pattern: true,
|
||||||
order_number_pattern: true,
|
order_number_pattern: true,
|
||||||
invoice_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_prefix: true,
|
||||||
warehouse_receipt_number_pattern: true,
|
warehouse_receipt_number_pattern: true,
|
||||||
warehouse_issue_prefix: true,
|
warehouse_issue_prefix: true,
|
||||||
@@ -234,6 +238,10 @@ export default async function companySettingsRoutes(
|
|||||||
offer_number_pattern: true,
|
offer_number_pattern: true,
|
||||||
order_number_pattern: true,
|
order_number_pattern: true,
|
||||||
invoice_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_prefix: true,
|
||||||
warehouse_receipt_number_pattern: true,
|
warehouse_receipt_number_pattern: true,
|
||||||
warehouse_issue_prefix: true,
|
warehouse_issue_prefix: true,
|
||||||
@@ -286,6 +294,10 @@ export default async function companySettingsRoutes(
|
|||||||
offer_number_pattern: true,
|
offer_number_pattern: true,
|
||||||
order_number_pattern: true,
|
order_number_pattern: true,
|
||||||
invoice_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_prefix: true,
|
||||||
warehouse_receipt_number_pattern: true,
|
warehouse_receipt_number_pattern: true,
|
||||||
warehouse_issue_prefix: true,
|
warehouse_issue_prefix: true,
|
||||||
@@ -310,7 +322,6 @@ export default async function companySettingsRoutes(
|
|||||||
settings.custom_fields as string | null,
|
settings.custom_fields as string | null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const pkg = require("../../../package.json") as { version: string };
|
const pkg = require("../../../package.json") as { version: string };
|
||||||
|
|
||||||
let available_vat_rates: number[] = [0, 10, 12, 15, 21];
|
let available_vat_rates: number[] = [0, 10, 12, 15, 21];
|
||||||
@@ -354,7 +365,6 @@ export default async function companySettingsRoutes(
|
|||||||
"/system-info",
|
"/system-info",
|
||||||
{ preHandler: requirePermission("settings.system") },
|
{ preHandler: requirePermission("settings.system") },
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
|
|
||||||
const pkg = require("../../../package.json") as { version: string };
|
const pkg = require("../../../package.json") as { version: string };
|
||||||
const uptimeSec = process.uptime();
|
const uptimeSec = process.uptime();
|
||||||
const days = Math.floor(uptimeSec / 86400);
|
const days = Math.floor(uptimeSec / 86400);
|
||||||
@@ -452,6 +462,10 @@ export default async function companySettingsRoutes(
|
|||||||
"offer_number_pattern",
|
"offer_number_pattern",
|
||||||
"order_number_pattern",
|
"order_number_pattern",
|
||||||
"invoice_number_pattern",
|
"invoice_number_pattern",
|
||||||
|
"issued_order_number_pattern",
|
||||||
|
"issued_order_type_code",
|
||||||
|
"project_number_pattern",
|
||||||
|
"project_type_code",
|
||||||
"warehouse_receipt_prefix",
|
"warehouse_receipt_prefix",
|
||||||
"warehouse_receipt_number_pattern",
|
"warehouse_receipt_number_pattern",
|
||||||
"warehouse_issue_prefix",
|
"warehouse_issue_prefix",
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export const UpdateCompanySettingsSchema = z.object({
|
|||||||
offer_number_pattern: z.string().nullish(),
|
offer_number_pattern: z.string().nullish(),
|
||||||
order_number_pattern: z.string().nullish(),
|
order_number_pattern: z.string().nullish(),
|
||||||
invoice_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_prefix: z.string().nullish(),
|
||||||
warehouse_receipt_number_pattern: z.string().nullish(),
|
warehouse_receipt_number_pattern: z.string().nullish(),
|
||||||
warehouse_issue_prefix: z.string().nullish(),
|
warehouse_issue_prefix: z.string().nullish(),
|
||||||
|
|||||||
Reference in New Issue
Block a user