feat(buyer-tasks): surface email_error on buyer task groups

Adds an emailError field on BuyerTaskGroup so the UI can render
the send-pipeline failure reason. The value is truncated to 200
chars at the service boundary to keep the card layout bounded.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-03 10:31:33 +02:00
parent 83bfd294b7
commit 964b3d51b2
2 changed files with 41 additions and 0 deletions

View file

@ -284,6 +284,34 @@ describe("listBuyerTasks", () => {
expect(groups[0].supplierEmail).toBe("orders@steelco.example");
});
it("populates emailError from the related PO when the PO has an email_error", async () => {
const { admin } = makeAdmin([
baseItem({
purchase_order: "po-1",
item_status: "po_generated",
expand: {
order: baseOrder,
supplier: baseSupplier,
purchase_order: {
id: "po-1",
status: "draft",
po_number: "PO-26080301",
email_error: "[webhook] Webhook returned HTTP 502",
expand: {
supplier: { id: "sup-1", email: "orders@steelco.example" },
},
},
},
}),
]);
vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never);
const groups = await listBuyerTasks("po_draft");
expect(groups).toHaveLength(1);
expect(groups[0].emailError).toBe("[webhook] Webhook returned HTTP 502");
});
it("leaves poNumber and supplierEmail null when the PO has no expand", async () => {
const { admin } = makeAdmin([
baseItem({

View file

@ -27,6 +27,12 @@ export type BuyerTaskGroup = {
poStatus: "draft" | "sent" | "sending" | "received" | "mail_failed" | "cancelled" | null;
poNumber: string | null;
supplierEmail: string | null;
/** Last send-pipeline error. Null on a clean draft or after a successful send.
* Pulled from `purchase_orders.email_error` and truncated to 200 chars at
* the service boundary so a runaway SMTP error doesn't blow up the card
* layout. The full reason remains visible in the order page's send-modal
* banner. */
emailError: string | null;
step: BuyerTaskTab;
};
@ -103,6 +109,13 @@ export async function listBuyerTasks(tab: BuyerTaskTab): Promise<BuyerTaskGroup[
poStatus: po?.status ?? null,
poNumber: po && typeof po === "object" && "po_number" in po ? String((po as { po_number?: string }).po_number ?? "") || null : null,
supplierEmail: (po as { expand?: { supplier?: { email?: string } } } | undefined)?.expand?.supplier?.email ?? null,
// Truncate the SMTP error to 200 chars at the service boundary so
// a runaway error string can't blow up the card layout. The full
// reason is still visible in the order page's send-modal banner.
emailError:
(po as { email_error?: string } | undefined)?.email_error
? String((po as { email_error?: string }).email_error).slice(0, 200)
: null,
step: tab,
};
groups.set(groupId, group);