feat(quotes): skip pricing checks in diagnosePdfReadiness when is_no_quote
Keeps the 'no supplier' and 'not processed' checks active so the function still flags legitimate data issues, but lets zero-priced items through when the operator has marked the quote as a decline.
This commit is contained in:
parent
e74ef505e7
commit
1d6f7538d8
2 changed files with 167 additions and 3 deletions
|
|
@ -1136,6 +1136,162 @@ describe("sendQuoteEmail", () => {
|
|||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* diagnosePdfReadiness is private to quotes-service.ts, but it's
|
||||
* reachable through sendQuoteEmail when the quote has no PDF. We use
|
||||
* that path to test the per-line readiness logic — the goal of these
|
||||
* cases is to verify that an `is_no_quote` quote can be sent even
|
||||
* though its items have zero prices / aren't processed, while the
|
||||
* "no supplier" gate stays active (a real data-quality issue even
|
||||
* on a decline-quote).
|
||||
*/
|
||||
describe("diagnosePdfReadiness with is_no_quote", () => {
|
||||
async function seedForReadinessCheck(opts: {
|
||||
isNoQuote: boolean;
|
||||
items: Array<{
|
||||
description: string;
|
||||
cost_price?: number;
|
||||
selling_price?: number;
|
||||
is_processed?: boolean;
|
||||
supplier?: string;
|
||||
}>;
|
||||
}) {
|
||||
const customer = await pbMock.collection("customers").create({
|
||||
customer: "Acme Mining",
|
||||
email: "buyer@acme.co.za",
|
||||
alternative_email: "",
|
||||
buyer_email: "",
|
||||
number: "",
|
||||
company: "Acme",
|
||||
dateadded: "",
|
||||
lastvisiteddate: "",
|
||||
rep: "",
|
||||
coordinates: "",
|
||||
description: "",
|
||||
});
|
||||
const q = await pbMock.collection("quotes").create({
|
||||
quote_number: "Q-2026-06-NOQ",
|
||||
customer: customer.id,
|
||||
status: "approval_pending",
|
||||
is_no_quote: opts.isNoQuote,
|
||||
notes: "",
|
||||
email_sender: "",
|
||||
email_subject: "",
|
||||
email_body: "",
|
||||
has_attachments: false,
|
||||
company_letterhead: "CNL",
|
||||
});
|
||||
// No PDF — force the diagnosePdfReadiness path inside sendQuoteEmail.
|
||||
for (const it of opts.items) {
|
||||
await pbMock.collection("quote_items").create({
|
||||
quote: q.id,
|
||||
quote_number: "Q-2026-06-NOQ",
|
||||
description: it.description,
|
||||
quantity: 1,
|
||||
cost_price: it.cost_price ?? 0,
|
||||
selling_price: it.selling_price ?? 0,
|
||||
markup: 0,
|
||||
supplier: it.supplier ?? "",
|
||||
is_processed: it.is_processed ?? false,
|
||||
sku: "",
|
||||
unit: "EA",
|
||||
notes: "",
|
||||
pricing_source: "manual",
|
||||
});
|
||||
}
|
||||
return { quote: q, customer };
|
||||
}
|
||||
|
||||
it("returns no items_not_ready issue when is_no_quote is true and zero prices", async () => {
|
||||
const { quote } = await seedForReadinessCheck({
|
||||
isNoQuote: true,
|
||||
items: [
|
||||
{
|
||||
description: "M16 bolt",
|
||||
cost_price: 0,
|
||||
selling_price: 0,
|
||||
is_processed: false,
|
||||
supplier: "sup-acme",
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await sendQuoteEmail(quote.id, {
|
||||
cc: "",
|
||||
bcc: "",
|
||||
subject: "",
|
||||
message: "",
|
||||
attachAuthLetters: false,
|
||||
selectedAttachmentIds: [],
|
||||
});
|
||||
// With the relaxation, the per-line pricing/processed checks are
|
||||
// skipped — and the supplier field is set so no other issue fires.
|
||||
// The diagnosePdfReadiness function falls through to "no_pdf".
|
||||
expect(result.failure?.code).not.toBe("items_not_ready");
|
||||
expect(result.failure?.code).toBe("no_pdf");
|
||||
});
|
||||
|
||||
it("still flags no supplier when is_no_quote is true", async () => {
|
||||
const { quote } = await seedForReadinessCheck({
|
||||
isNoQuote: true,
|
||||
items: [
|
||||
{
|
||||
description: "M16 bolt",
|
||||
cost_price: 0,
|
||||
selling_price: 0,
|
||||
is_processed: false,
|
||||
// supplier intentionally blank — must still flag.
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await sendQuoteEmail(quote.id, {
|
||||
cc: "",
|
||||
bcc: "",
|
||||
subject: "",
|
||||
message: "",
|
||||
attachAuthLetters: false,
|
||||
selectedAttachmentIds: [],
|
||||
});
|
||||
expect(result.failure?.code).toBe("items_not_ready");
|
||||
expect(result.failure?.details?.issues?.[0]?.missing).toContain(
|
||||
"no supplier"
|
||||
);
|
||||
// And the pricing/processed checks must NOT appear in the missing list
|
||||
// (since the operator marked this as a decline).
|
||||
const missing = result.failure?.details?.issues?.[0]?.missing ?? [];
|
||||
expect(missing).not.toContain("cost price");
|
||||
expect(missing).not.toContain("selling price");
|
||||
expect(missing).not.toContain("not processed");
|
||||
});
|
||||
|
||||
it("still flags items_not_ready when is_no_quote is false and zero prices (regression)", async () => {
|
||||
const { quote } = await seedForReadinessCheck({
|
||||
isNoQuote: false,
|
||||
items: [
|
||||
{
|
||||
description: "M16 bolt",
|
||||
cost_price: 0,
|
||||
selling_price: 0,
|
||||
is_processed: false,
|
||||
supplier: "sup-acme",
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await sendQuoteEmail(quote.id, {
|
||||
cc: "",
|
||||
bcc: "",
|
||||
subject: "",
|
||||
message: "",
|
||||
attachAuthLetters: false,
|
||||
selectedAttachmentIds: [],
|
||||
});
|
||||
expect(result.failure?.code).toBe("items_not_ready");
|
||||
const missing = result.failure?.details?.issues?.[0]?.missing ?? [];
|
||||
expect(missing).toContain("not processed");
|
||||
expect(missing).toContain("cost price");
|
||||
expect(missing).toContain("selling price");
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryQuoteEmail", () => {
|
||||
it("flips a mail_failed quote back to sending and clears error", async () => {
|
||||
const { quote } = await seedReadyQuote({ status: "mail_failed" });
|
||||
|
|
|
|||
|
|
@ -1832,10 +1832,18 @@ async function diagnosePdfReadiness(
|
|||
}> = [];
|
||||
for (const it of allItems) {
|
||||
const missing: string[] = [];
|
||||
if (!it.is_processed) missing.push("not processed");
|
||||
// Pricing / processed checks are skipped when the operator has
|
||||
// marked this quote as a decline (`is_no_quote`) — they explicitly
|
||||
// don't want a priced PDF. The supplier check stays active:
|
||||
// "no supplier" is a data-quality issue regardless of pricing
|
||||
// intent, and an unpriced item with no supplier means the line
|
||||
// was never even looked at.
|
||||
if (!quote.is_no_quote) {
|
||||
if (!it.is_processed) missing.push("not processed");
|
||||
if (!(Number(it.cost_price) > 0)) missing.push("cost price");
|
||||
if (!(Number(it.selling_price) > 0)) missing.push("selling price");
|
||||
}
|
||||
if (!it.supplier) missing.push("no supplier");
|
||||
if (!(Number(it.cost_price) > 0)) missing.push("cost price");
|
||||
if (!(Number(it.selling_price) > 0)) missing.push("selling price");
|
||||
if (missing.length > 0) {
|
||||
issues.push({
|
||||
itemId: String(it.id),
|
||||
|
|
|
|||
Loading…
Reference in a new issue