diff --git a/src/lib/server/stock-purchases-service.test.ts b/src/lib/server/stock-purchases-service.test.ts index f7ef072..0aa4a26 100644 --- a/src/lib/server/stock-purchases-service.test.ts +++ b/src/lib/server/stock-purchases-service.test.ts @@ -179,6 +179,7 @@ import { getStockPurchaseItems, receiveStockPurchase, markStockPurchaseSent, + recordStockPurchaseSendOutcome, } from "./stock-purchases-service"; beforeEach(() => { @@ -812,3 +813,92 @@ describe("listStockPurchases — total value", () => { expect(y.received_value).toBe(100); }); }); + +// ============================================================================ +// recordStockPurchaseSendOutcome +// ============================================================================ + +describe("recordStockPurchaseSendOutcome", () => { + it("does NOT auto-flip stock_purchase_items.status on success (operator must mark them ready to collect manually)", async () => { + const p = await pbMock.collection("stock_purchases").create({ + purchase_number: "PO26071501STK", + status: "sending", + email_payload: { sentAt: "", sentMessageId: "" }, + }); + const it1 = await pbMock.collection("stock_purchase_items").create({ + purchase: p.id, + status: "collection_pool", + }); + const it2 = await pbMock.collection("stock_purchase_items").create({ + purchase: p.id, + status: "collection_pool", + }); + + const result = await recordStockPurchaseSendOutcome({ + purchaseId: p.id, + outcome: "success", + messageId: "", + }); + + // The stock_purchase itself flips to "sent" + stamps the message ID. + expect(result.status).toBe("sent"); + expect(result.email_message_id).toBe(""); + + // The items MUST stay in collection_pool — the operator marks them + // ready-to-collect from the stock-order page only after the supplier + // has confirmed. Same pattern as customer POs. + const itemsAfter = await pbMock + .collection("stock_purchase_items") + .getFullList({ filter: `purchase = "${p.id}"` }); + expect(itemsAfter.find((i) => i.id === it1.id)?.status).toBe("collection_pool"); + expect(itemsAfter.find((i) => i.id === it2.id)?.status).toBe("collection_pool"); + }); + + it("flips stock_purchase to sent + stamps email_message_id on success", async () => { + const p = await pbMock.collection("stock_purchases").create({ + purchase_number: "PO26071501STK", + status: "sending", + email_payload: { sentAt: "", sentMessageId: "" }, + }); + + const result = await recordStockPurchaseSendOutcome({ + purchaseId: p.id, + outcome: "success", + messageId: "", + }); + + expect(result.status).toBe("sent"); + expect(result.email_message_id).toBe(""); + expect(result.email_error).toBe(""); + expect((result.email_payload as Record).sentAt).toBeTruthy(); + expect((result.email_payload as Record).sentMessageId).toBe( + "", + ); + }); + + it("flips stock_purchase to mail_failed + stamps errorMessage on failure (does not touch items)", async () => { + const p = await pbMock.collection("stock_purchases").create({ + purchase_number: "PO26071501STK", + status: "sending", + }); + const it1 = await pbMock.collection("stock_purchase_items").create({ + purchase: p.id, + status: "collection_pool", + }); + + const result = await recordStockPurchaseSendOutcome({ + purchaseId: p.id, + outcome: "failure", + errorMessage: "SMTP 550 mailbox unavailable", + }); + + expect(result.status).toBe("mail_failed"); + expect(result.email_error).toBe("[smtp] SMTP 550 mailbox unavailable"); + + // Items are NOT touched on failure (and were never touched to begin with). + const itemsAfter = await pbMock + .collection("stock_purchase_items") + .getFullList({ filter: `purchase = "${p.id}"` }); + expect(itemsAfter.find((i) => i.id === it1.id)?.status).toBe("collection_pool"); + }); +}); diff --git a/src/lib/server/stock-purchases-service.ts b/src/lib/server/stock-purchases-service.ts index ce110e4..01f8c9f 100644 --- a/src/lib/server/stock-purchases-service.ts +++ b/src/lib/server/stock-purchases-service.ts @@ -1036,11 +1036,13 @@ async function markPreSendFailure( * after the email goes out (success or failure). * * `outcome = "success"` flips status to `sent` + stamps delivery - * fields + flips linked `stock_purchase_items.status` from - * `collection_pool` to `assigned_to_driver` (so the driver-app - * can pick them up). Note: stock lines use `assigned_to_driver` - * instead of `assigned` because their per-line state machine has - * different values than the customer side. + * fields. It deliberately does NOT touch linked + * `stock_purchase_items.status` — the operator marks items + * ready-to-collect manually from the stock-order page after the + * supplier confirms, which then moves them to `assigned_to_driver`. + * Auto-flipping on send would hide the item before the supplier has + * even confirmed (mirrors the customer-PO behaviour adopted in + * commit 79b9309). * * `outcome = "failure"` flips to `mail_failed` + stamps error. */ @@ -1081,29 +1083,19 @@ export async function recordStockPurchaseSendOutcome( .collection(STOCK_PURCHASES_COLL) .update(input.purchaseId, patch)) as StockPurchase; - // Flip linked stock_purchase_items from `collection_pool` to - // `assigned_to_driver`. We don't touch `drv_id` — that's set - // later by the assign-driver action on /collection-pool. Status - // flip here just signals "PO is in flight; ready for the operator - // to assign a driver when the supplier confirms". + // Stock_purchase_items stay in `collection_pool` after the send. + // The operator marks them ready-to-collect from the stock-order + // page after the supplier confirms, which is what moves them to + // `assigned_to_driver` (so the driver-app can pick them up). + // We don't touch `drv_id` here — that's set later by the + // assign-driver action on /collection-pool. The status names + // align with the v4 stock-order driver flow: + // collection_pool → assigned_to_driver → collected → booked. // - // v1 note: this mirrors the old `markStockPurchaseSent` behaviour - // (which also flipped all lines to collection_pool at the time - // of send). The status names align with the new v4 stock-order - // driver flow: collection_pool → assigned_to_driver → collected - // → booked. - const linked = await pb - .collection(STOCK_PURCHASE_ITEMS_COLL) - .getFullList({ - filter: `purchase = "${input.purchaseId}" && status = "collection_pool"`, - fields: "id,status", - }); - for (const it of linked) { - await pb.collection(STOCK_PURCHASE_ITEMS_COLL).update(it.id, { - status: "assigned_to_driver", - }); - } - + // v1 note: this used to auto-flip on send (mirroring the old + // `markStockPurchaseSent` behaviour), but that hid items before + // the supplier had even confirmed. Now consistent with customer + // POs (see commit 79b9309). return updated; }