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:
@@ -3,6 +3,9 @@ import {
|
|||||||
encodeSelectedCustomFields,
|
encodeSelectedCustomFields,
|
||||||
parseSelectedCustomFields,
|
parseSelectedCustomFields,
|
||||||
} from "../utils/custom-fields";
|
} 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", () => {
|
describe("selected custom fields encode/decode", () => {
|
||||||
it("encodes a non-empty index array to a JSON string", () => {
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
releaseInvoiceNumber,
|
releaseInvoiceNumber,
|
||||||
assignInvoiceNumber,
|
assignInvoiceNumber,
|
||||||
} from "./numbering.service";
|
} from "./numbering.service";
|
||||||
|
import {
|
||||||
|
encodeSelectedCustomFields,
|
||||||
|
parseSelectedCustomFields,
|
||||||
|
} from "../utils/custom-fields";
|
||||||
|
|
||||||
// Status transition rules matching PHP.
|
// Status transition rules matching PHP.
|
||||||
// draft -> issued is the finalize step (consumes + assigns the invoice number).
|
// 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,
|
customer_name: invoice.customers?.name || null,
|
||||||
order_number: invoice.orders?.order_number || null,
|
order_number: invoice.orders?.order_number || null,
|
||||||
valid_transitions: VALID_TRANSITIONS[invoice.status as string] || [],
|
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
|
internal_notes: body.internal_notes
|
||||||
? String(body.internal_notes)
|
? String(body.internal_notes)
|
||||||
: null,
|
: 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;
|
data.due_date = body.due_date ? new Date(String(body.due_date)) : null;
|
||||||
if (body.tax_date !== undefined)
|
if (body.tax_date !== undefined)
|
||||||
data.tax_date = body.tax_date ? new Date(String(body.tax_date)) : null;
|
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)
|
// Internal notes editable in draft/issued/overdue (never printed)
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import {
|
|||||||
isIssuedOrderNumberTaken,
|
isIssuedOrderNumberTaken,
|
||||||
} from "./numbering.service";
|
} from "./numbering.service";
|
||||||
import { nasOrdersManager } from "./nas-financials-manager";
|
import { nasOrdersManager } from "./nas-financials-manager";
|
||||||
|
import {
|
||||||
|
encodeSelectedCustomFields,
|
||||||
|
parseSelectedCustomFields,
|
||||||
|
} from "../utils/custom-fields";
|
||||||
|
|
||||||
export interface IssuedOrderItemInput {
|
export interface IssuedOrderItemInput {
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
@@ -253,6 +257,9 @@ export async function getIssuedOrder(id: number) {
|
|||||||
supplier: order.suppliers,
|
supplier: order.suppliers,
|
||||||
supplier_name: order.suppliers?.name || null,
|
supplier_name: order.suppliers?.name || null,
|
||||||
valid_transitions: VALID_TRANSITIONS[order.status as string] || [],
|
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
|
internal_notes: body.internal_notes
|
||||||
? String(body.internal_notes)
|
? String(body.internal_notes)
|
||||||
: null,
|
: 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
|
data.internal_notes = body.internal_notes
|
||||||
? String(body.internal_notes)
|
? String(body.internal_notes)
|
||||||
: null;
|
: 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);
|
if (body.status !== undefined) data.status = String(body.status);
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import {
|
|||||||
assignOfferNumber,
|
assignOfferNumber,
|
||||||
} from "./numbering.service";
|
} from "./numbering.service";
|
||||||
import { nasOffersManager } from "./nas-offers-manager";
|
import { nasOffersManager } from "./nas-offers-manager";
|
||||||
|
import {
|
||||||
|
encodeSelectedCustomFields,
|
||||||
|
parseSelectedCustomFields,
|
||||||
|
} from "../utils/custom-fields";
|
||||||
|
|
||||||
interface QuotationItemInput {
|
interface QuotationItemInput {
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -283,6 +287,9 @@ export async function getOffer(id: number) {
|
|||||||
// Computed server-side so the frontend renders only legal status buttons
|
// Computed server-side so the frontend renders only legal status buttons
|
||||||
// (same contract as getIssuedOrder).
|
// (same contract as getIssuedOrder).
|
||||||
valid_transitions: VALID_TRANSITIONS[quotation.status] || [],
|
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
|
scope_description: body.scope_description
|
||||||
? String(body.scope_description)
|
? String(body.scope_description)
|
||||||
: null,
|
: 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)
|
? String(body.scope_description)
|
||||||
: null
|
: null
|
||||||
: undefined,
|
: undefined,
|
||||||
|
selected_custom_fields:
|
||||||
|
body.selected_custom_fields !== undefined
|
||||||
|
? encodeSelectedCustomFields(body.selected_custom_fields)
|
||||||
|
: undefined,
|
||||||
modified_at: new Date(),
|
modified_at: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user