From 0d9b5e957071546489bc724f557bfdcd50b8d97c Mon Sep 17 00:00:00 2001 From: BOHA Date: Tue, 16 Jun 2026 20:11:10 +0200 Subject: [PATCH] feat(documents): persist & expose selected_custom_fields in services Co-Authored-By: Claude Opus 4.8 (1M context) --- src/__tests__/selected-custom-fields.test.ts | 35 ++++++++++++++++++++ src/services/invoices.service.ts | 14 ++++++++ src/services/issued-orders.service.ts | 14 ++++++++ src/services/offers.service.ts | 14 ++++++++ 4 files changed, 77 insertions(+) diff --git a/src/__tests__/selected-custom-fields.test.ts b/src/__tests__/selected-custom-fields.test.ts index 74801a9..00f707d 100644 --- a/src/__tests__/selected-custom-fields.test.ts +++ b/src/__tests__/selected-custom-fields.test.ts @@ -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(); + }); +}); diff --git a/src/services/invoices.service.ts b/src/services/invoices.service.ts index 175f9b5..654f343 100644 --- a/src/services/invoices.service.ts +++ b/src/services/invoices.service.ts @@ -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) diff --git a/src/services/issued-orders.service.ts b/src/services/issued-orders.service.ts index 2247430..4265942 100644 --- a/src/services/issued-orders.service.ts +++ b/src/services/issued-orders.service.ts @@ -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); diff --git a/src/services/offers.service.ts b/src/services/offers.service.ts index da66514..3842a3e 100644 --- a/src/services/offers.service.ts +++ b/src/services/offers.service.ts @@ -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) { 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(body.scope_description) : null : undefined, + selected_custom_fields: + body.selected_custom_fields !== undefined + ? encodeSelectedCustomFields(body.selected_custom_fields) + : undefined, modified_at: new Date(), };