feat(documents): persist & expose selected_custom_fields in services

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-16 20:11:10 +02:00
parent 2b3266a1cc
commit 0d9b5e9570
4 changed files with 77 additions and 0 deletions

View File

@@ -3,6 +3,9 @@ import {
encodeSelectedCustomFields,
parseSelectedCustomFields,
} from "../utils/custom-fields";
import { afterAll } from "vitest";
import { createOffer, getOffer } from "../services/offers.service";
import prisma from "../config/database";
describe("selected custom fields encode/decode", () => {
it("encodes a non-empty index array to a JSON string", () => {
@@ -36,3 +39,35 @@ describe("selected custom fields encode/decode", () => {
]);
});
});
describe("offers selected_custom_fields round-trip", () => {
const created: number[] = [];
afterAll(async () => {
if (created.length)
await prisma.quotations.deleteMany({ where: { id: { in: created } } });
});
it("persists selection on create and decodes it on detail", async () => {
const res = (await createOffer({
status: "draft",
selected_custom_fields: [2, 0, 0],
})) as { id: number };
created.push(res.id);
const row = await prisma.quotations.findUnique({ where: { id: res.id } });
expect(row?.selected_custom_fields).toBe("[0,2]");
const detail = await getOffer(res.id);
expect(detail?.selected_custom_fields).toEqual([0, 2]);
});
it("stores null when selection is empty", async () => {
const res = (await createOffer({
status: "draft",
selected_custom_fields: [],
})) as { id: number };
created.push(res.id);
const row = await prisma.quotations.findUnique({ where: { id: res.id } });
expect(row?.selected_custom_fields).toBeNull();
});
});

View File

@@ -7,6 +7,10 @@ import {
releaseInvoiceNumber,
assignInvoiceNumber,
} from "./numbering.service";
import {
encodeSelectedCustomFields,
parseSelectedCustomFields,
} from "../utils/custom-fields";
// Status transition rules matching PHP.
// draft -> issued is the finalize step (consumes + assigns the invoice number).
@@ -512,6 +516,9 @@ export async function getInvoice(id: number) {
customer_name: invoice.customers?.name || null,
order_number: invoice.orders?.order_number || null,
valid_transitions: VALID_TRANSITIONS[invoice.status as string] || [],
selected_custom_fields: parseSelectedCustomFields(
rest.selected_custom_fields,
),
};
}
@@ -561,6 +568,9 @@ export async function createInvoice(body: InvoiceInput) {
internal_notes: body.internal_notes
? String(body.internal_notes)
: null,
selected_custom_fields: encodeSelectedCustomFields(
body.selected_custom_fields,
),
},
});
@@ -652,6 +662,10 @@ export async function updateInvoice(id: number, body: InvoiceInput) {
data.due_date = body.due_date ? new Date(String(body.due_date)) : null;
if (body.tax_date !== undefined)
data.tax_date = body.tax_date ? new Date(String(body.tax_date)) : null;
if (body.selected_custom_fields !== undefined)
data.selected_custom_fields = encodeSelectedCustomFields(
body.selected_custom_fields,
);
}
// Internal notes editable in draft/issued/overdue (never printed)

View File

@@ -9,6 +9,10 @@ import {
isIssuedOrderNumberTaken,
} from "./numbering.service";
import { nasOrdersManager } from "./nas-financials-manager";
import {
encodeSelectedCustomFields,
parseSelectedCustomFields,
} from "../utils/custom-fields";
export interface IssuedOrderItemInput {
description?: string | null;
@@ -253,6 +257,9 @@ export async function getIssuedOrder(id: number) {
supplier: order.suppliers,
supplier_name: order.suppliers?.name || null,
valid_transitions: VALID_TRANSITIONS[order.status as string] || [],
selected_custom_fields: parseSelectedCustomFields(
rest.selected_custom_fields,
),
};
}
@@ -319,6 +326,9 @@ export async function createIssuedOrder(body: IssuedOrderInput) {
internal_notes: body.internal_notes
? String(body.internal_notes)
: null,
selected_custom_fields: encodeSelectedCustomFields(
body.selected_custom_fields,
),
},
});
@@ -424,6 +434,10 @@ export async function updateIssuedOrder(id: number, body: IssuedOrderInput) {
data.internal_notes = body.internal_notes
? String(body.internal_notes)
: null;
if (body.selected_custom_fields !== undefined)
data.selected_custom_fields = encodeSelectedCustomFields(
body.selected_custom_fields,
);
}
if (body.status !== undefined) data.status = String(body.status);

View File

@@ -8,6 +8,10 @@ import {
assignOfferNumber,
} from "./numbering.service";
import { nasOffersManager } from "./nas-offers-manager";
import {
encodeSelectedCustomFields,
parseSelectedCustomFields,
} from "../utils/custom-fields";
interface QuotationItemInput {
description?: string;
@@ -283,6 +287,9 @@ export async function getOffer(id: number) {
// Computed server-side so the frontend renders only legal status buttons
// (same contract as getIssuedOrder).
valid_transitions: VALID_TRANSITIONS[quotation.status] || [],
selected_custom_fields: parseSelectedCustomFields(
rest.selected_custom_fields,
),
};
}
@@ -342,6 +349,9 @@ export async function createOffer(body: Record<string, unknown>) {
scope_description: body.scope_description
? String(body.scope_description)
: null,
selected_custom_fields: encodeSelectedCustomFields(
body.selected_custom_fields,
),
},
});
@@ -472,6 +482,10 @@ export async function updateOffer(id: number, body: Record<string, unknown>) {
? String(body.scope_description)
: null
: undefined,
selected_custom_fields:
body.selected_custom_fields !== undefined
? encodeSelectedCustomFields(body.selected_custom_fields)
: undefined,
modified_at: new Date(),
};