feat(issued-orders)!: sections replace flat terms/notes fields; Obsah title
Per user decision the rich-text sections now carry all free-form PDF content on issued orders: dropped columns delivery_terms, payment_terms, issued_by, notes (migration applied to dev+test) and their PDF blocks + form fields. Kept: order_text (moved from the bottom card into the Zakladni udaje grid, replacing the Vystavil field) and internal_notes (now the only field in the bottom card, never printed). Sections card is titled 'Obsah' on issued orders; offers keep 'Rozsah projektu' and are untouched. Legacy clients sending the dropped keys are silently stripped (tested). PDF footer 'Vystavil:' stays - it prints the logged-in user, not the dropped column. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -481,6 +481,36 @@ describe("POST /api/admin/issued-orders supplier validation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/admin/issued-orders legacy dropped fields", () => {
|
||||
it("silently ignores the removed flat-text keys (old clients keep working)", async () => {
|
||||
// delivery_terms/payment_terms/issued_by/notes were dropped from the
|
||||
// schema AND the table — a stale client still sending them must save
|
||||
// cleanly (non-strict Zod strips unknown keys) with the kept fields intact.
|
||||
const res = await app!.inject({
|
||||
method: "POST",
|
||||
url: "/api/admin/issued-orders",
|
||||
headers: { Authorization: `Bearer ${adminToken}` },
|
||||
payload: {
|
||||
notes: "<p>stará poznámka</p>",
|
||||
delivery_terms: "DAP, sklad odběratele",
|
||||
payment_terms: "splatnost 14 dní",
|
||||
issued_by: "Starý klient",
|
||||
order_text: "Objednáváme dle smlouvy:",
|
||||
},
|
||||
});
|
||||
expect(res.statusCode).toBe(201);
|
||||
const body = res.json();
|
||||
expect(body.success).toBe(true);
|
||||
createdIds.push(body.data.id);
|
||||
|
||||
const row = await prisma.issued_orders.findUnique({
|
||||
where: { id: body.data.id },
|
||||
});
|
||||
expect(row).not.toBeNull();
|
||||
expect(row!.order_text).toBe("Objednáváme dle smlouvy:");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/admin/issued-orders/:id editable-state guard (HTTP)", () => {
|
||||
it("400s a field edit on a confirmed order with the explicit Czech message", async () => {
|
||||
const order = await mkIssued({});
|
||||
@@ -491,7 +521,7 @@ describe("PUT /api/admin/issued-orders/:id editable-state guard (HTTP)", () => {
|
||||
method: "PUT",
|
||||
url: `/api/admin/issued-orders/${order.id}`,
|
||||
headers: { Authorization: `Bearer ${adminToken}` },
|
||||
payload: { notes: "pokus o úpravu" },
|
||||
payload: { order_text: "pokus o úpravu" },
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(res.json().error).toBe("Objednávku v tomto stavu nelze upravovat");
|
||||
@@ -518,12 +548,12 @@ describe("PUT /api/admin/issued-orders/:id editable-state guard (HTTP)", () => {
|
||||
|
||||
describe("issued-order audit old/new values", () => {
|
||||
it("PUT writes an audit row carrying oldValues and newValues", async () => {
|
||||
const order = await mkIssued({ notes: "před úpravou" });
|
||||
const order = await mkIssued({ internal_notes: "před úpravou" });
|
||||
const res = await app!.inject({
|
||||
method: "PUT",
|
||||
url: `/api/admin/issued-orders/${order.id}`,
|
||||
headers: { Authorization: `Bearer ${adminToken}` },
|
||||
payload: { notes: "po úpravě" },
|
||||
payload: { internal_notes: "po úpravě" },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
|
||||
@@ -538,15 +568,15 @@ describe("issued-order audit old/new values", () => {
|
||||
expect(audit).not.toBeNull();
|
||||
expect(audit!.old_values).toBeTruthy();
|
||||
expect(audit!.new_values).toBeTruthy();
|
||||
expect(JSON.parse(audit!.old_values!).notes).toBe("před úpravou");
|
||||
expect(JSON.parse(audit!.new_values!).notes).toBe("po úpravě");
|
||||
expect(JSON.parse(audit!.old_values!).internal_notes).toBe("před úpravou");
|
||||
expect(JSON.parse(audit!.new_values!).internal_notes).toBe("po úpravě");
|
||||
// Unnumbered drafts read "koncept", never "null".
|
||||
expect(audit!.description).toContain("koncept");
|
||||
expect(audit!.description).not.toContain("null");
|
||||
});
|
||||
|
||||
it("DELETE writes an audit row carrying oldValues", async () => {
|
||||
const order = await mkIssued({ notes: "ke smazání" });
|
||||
const order = await mkIssued({ internal_notes: "ke smazání" });
|
||||
const res = await app!.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/admin/issued-orders/${order.id}`,
|
||||
@@ -563,7 +593,7 @@ describe("issued-order audit old/new values", () => {
|
||||
orderBy: { id: "desc" },
|
||||
});
|
||||
expect(audit).not.toBeNull();
|
||||
expect(JSON.parse(audit!.old_values!).notes).toBe("ke smazání");
|
||||
expect(JSON.parse(audit!.old_values!).internal_notes).toBe("ke smazání");
|
||||
expect(audit!.description).toContain("koncept");
|
||||
});
|
||||
});
|
||||
@@ -612,10 +642,6 @@ describe("renderIssuedOrderHtml", () => {
|
||||
order_date: new Date("2026-06-09T12:00:00"),
|
||||
delivery_date: null,
|
||||
currency: "CZK",
|
||||
notes: "<p>Pozn</p><script>alert(1)</script>",
|
||||
delivery_terms: null,
|
||||
payment_terms: null,
|
||||
issued_by: null,
|
||||
};
|
||||
const items = [
|
||||
{
|
||||
@@ -690,10 +716,17 @@ describe("renderIssuedOrderHtml", () => {
|
||||
expect(html).not.toContain("DIČ: ");
|
||||
});
|
||||
|
||||
it("strips script tags from notes", () => {
|
||||
const html = renderIssuedOrderHtml(order, items, null, null, "cs", issuer);
|
||||
it("escapes HTML in the order_text heading (no raw tags reach the PDF)", () => {
|
||||
const html = renderIssuedOrderHtml(
|
||||
{ ...order, order_text: '<script>alert(1)</script>Text "X"' },
|
||||
items,
|
||||
null,
|
||||
null,
|
||||
"cs",
|
||||
issuer,
|
||||
);
|
||||
expect(html).not.toContain("<script>");
|
||||
expect(html).not.toContain("alert(1)");
|
||||
expect(html).toContain("<script>");
|
||||
});
|
||||
|
||||
it("renders the custom order_text heading when set, default when not", () => {
|
||||
@@ -846,7 +879,7 @@ describe("issued-order sections (service)", () => {
|
||||
describe("updateIssuedOrder atomicity (single transaction)", () => {
|
||||
it("rolls back the header write when the items replace fails", async () => {
|
||||
const order = await mkIssued({
|
||||
notes: "původní",
|
||||
internal_notes: "původní",
|
||||
items: [{ description: "A", quantity: 1, unit_price: 10 }],
|
||||
});
|
||||
|
||||
@@ -856,7 +889,7 @@ describe("updateIssuedOrder atomicity (single transaction)", () => {
|
||||
const tooLong = "x".repeat(600);
|
||||
await expect(
|
||||
updateIssuedOrder(order.id, {
|
||||
notes: "změněno",
|
||||
internal_notes: "změněno",
|
||||
items: [{ description: tooLong, quantity: 1, unit_price: 1 }],
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
@@ -864,7 +897,7 @@ describe("updateIssuedOrder atomicity (single transaction)", () => {
|
||||
const row = await prisma.issued_orders.findUnique({
|
||||
where: { id: order.id },
|
||||
});
|
||||
expect(row!.notes).toBe("původní");
|
||||
expect(row!.internal_notes).toBe("původní");
|
||||
const items = await prisma.issued_order_items.findMany({
|
||||
where: { issued_order_id: order.id },
|
||||
});
|
||||
@@ -1156,10 +1189,6 @@ describe("renderIssuedOrderHtml sections (scope page)", () => {
|
||||
order_date: new Date("2026-06-09T12:00:00"),
|
||||
delivery_date: null,
|
||||
currency: "CZK",
|
||||
notes: null,
|
||||
delivery_terms: null,
|
||||
payment_terms: null,
|
||||
issued_by: null,
|
||||
};
|
||||
const items = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user