feat(stock-quick-draft): add updateQuickDraftLine service
Adds a TDD'd updateQuickDraftLine function that PATCHes quantity and/or cost_price on a stock_purchase_items row. Returns the same discriminated-union shape as its sibling add/remove functions. No schema changes. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
f9a0d08dab
commit
da0a689507
2 changed files with 181 additions and 1 deletions
|
|
@ -160,8 +160,12 @@ const { store, pbMock, clearStore } = vi.hoisted(() => {
|
|||
return { store, pbMock, clearStore };
|
||||
});
|
||||
|
||||
vi.mock("pocketbase", () => ({ default: vi.fn().mockImplementation(() => pbMock) }));
|
||||
vi.mock("pocketbase", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("pocketbase")>();
|
||||
return { ...actual, default: vi.fn().mockImplementation(() => pbMock) };
|
||||
});
|
||||
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
import {
|
||||
addQuickDraftLine,
|
||||
getStockItemSupplierHistory,
|
||||
|
|
@ -169,6 +173,7 @@ import {
|
|||
removeQuickDraftLine,
|
||||
searchStockItemsForQuickDraft,
|
||||
listSuppliersForQuickDraft,
|
||||
updateQuickDraftLine,
|
||||
} from "./stock-quick-draft-service";
|
||||
|
||||
beforeEach(() => clearStore());
|
||||
|
|
@ -640,6 +645,132 @@ describe("removeQuickDraftLine", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("updateQuickDraftLine", () => {
|
||||
async function createDraftLine() {
|
||||
const supplierId = (
|
||||
await pbMock.collection("suppliers").create({ suppliername: "S" })
|
||||
).id;
|
||||
const purchase = await pbMock.collection("stock_purchases").create({
|
||||
supplier: supplierId,
|
||||
status: "draft",
|
||||
purchase_number: "PO26070101STK",
|
||||
});
|
||||
return pbMock.collection("stock_purchase_items").create({
|
||||
purchase: purchase.id,
|
||||
stock_item: "item-1",
|
||||
description: "115mm Cutting Disc",
|
||||
quantity: 10,
|
||||
cost_price: 18.5,
|
||||
});
|
||||
}
|
||||
|
||||
it("updates quantity on the line and returns ok", async () => {
|
||||
const line = await createDraftLine();
|
||||
|
||||
const result = await updateQuickDraftLine(pbMock as never, {
|
||||
lineId: line.id,
|
||||
quantity: 25,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
const updated = await pbMock.collection("stock_purchase_items").getOne(line.id);
|
||||
expect(updated.quantity).toBe(25);
|
||||
expect(updated.cost_price).toBe(18.5);
|
||||
});
|
||||
|
||||
it("updates cost_price on the line and returns ok", async () => {
|
||||
const line = await createDraftLine();
|
||||
|
||||
const result = await updateQuickDraftLine(pbMock as never, {
|
||||
lineId: line.id,
|
||||
costPrice: 20,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
const updated = await pbMock.collection("stock_purchase_items").getOne(line.id);
|
||||
expect(updated.quantity).toBe(10);
|
||||
expect(updated.cost_price).toBe(20);
|
||||
});
|
||||
|
||||
it("updates both fields in one call", async () => {
|
||||
const line = await createDraftLine();
|
||||
|
||||
const result = await updateQuickDraftLine(pbMock as never, {
|
||||
lineId: line.id,
|
||||
quantity: 30,
|
||||
costPrice: 21.5,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
const updated = await pbMock.collection("stock_purchase_items").getOne(line.id);
|
||||
expect(updated.quantity).toBe(30);
|
||||
expect(updated.cost_price).toBe(21.5);
|
||||
});
|
||||
|
||||
it("rejects when neither quantity nor costPrice is supplied", async () => {
|
||||
const line = await createDraftLine();
|
||||
|
||||
const result = await updateQuickDraftLine(pbMock as never, { lineId: line.id });
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.reason).toBe("validation");
|
||||
});
|
||||
|
||||
it("rejects non-positive quantity", async () => {
|
||||
const line = await createDraftLine();
|
||||
|
||||
const result = await updateQuickDraftLine(pbMock as never, {
|
||||
lineId: line.id,
|
||||
quantity: 0,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.reason).toBe("validation");
|
||||
});
|
||||
|
||||
it("rejects negative cost_price", async () => {
|
||||
const line = await createDraftLine();
|
||||
|
||||
const result = await updateQuickDraftLine(pbMock as never, {
|
||||
lineId: line.id,
|
||||
costPrice: -1,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.reason).toBe("validation");
|
||||
});
|
||||
|
||||
it("returns line_not_found when the line does not exist", async () => {
|
||||
const originalCollection = pbMock.collection;
|
||||
const collectionSpy = vi.spyOn(pbMock, "collection").mockImplementation((name) => {
|
||||
const collection = originalCollection(name);
|
||||
if (name !== "stock_purchase_items") return collection;
|
||||
return {
|
||||
...collection,
|
||||
update: vi.fn().mockRejectedValue(
|
||||
new ClientResponseError({
|
||||
status: 404,
|
||||
response: { message: "line not found" },
|
||||
}),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const result = await updateQuickDraftLine(pbMock as never, {
|
||||
lineId: "does-not-exist",
|
||||
quantity: 1,
|
||||
});
|
||||
collectionSpy.mockRestore();
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.reason).toBe("line_not_found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("listOpenDrafts", () => {
|
||||
it("returns an empty array when there are no draft stock_purchases", async () => {
|
||||
const drafts = await listOpenDrafts(pbMock as never);
|
||||
|
|
|
|||
|
|
@ -505,6 +505,55 @@ export async function removeQuickDraftLine(
|
|||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* updateQuickDraftLine
|
||||
* ========================================================================== */
|
||||
|
||||
export type UpdateQuickDraftLineResult =
|
||||
| { ok: true }
|
||||
| { ok: false; reason: "validation" | "line_not_found" | "update_failed"; message: string };
|
||||
|
||||
/**
|
||||
* Updates the quantity and/or cost price on a `stock_purchase_items` row.
|
||||
* Used by the modal's inline line editor for correcting draft values.
|
||||
*
|
||||
* Returns `{ ok: true }` on success, or `{ ok: false, reason, message }`
|
||||
* on validation or update failure. At least one field is required;
|
||||
* quantity must be positive and cost price must be non-negative.
|
||||
*/
|
||||
export async function updateQuickDraftLine(
|
||||
pb: StockQuickDraftPb,
|
||||
input: { lineId: string; quantity?: number; costPrice?: number },
|
||||
): Promise<UpdateQuickDraftLineResult> {
|
||||
if (typeof input.lineId !== "string" || input.lineId.length === 0) {
|
||||
return { ok: false, reason: "validation", message: "lineId is required" };
|
||||
}
|
||||
if (input.quantity === undefined && input.costPrice === undefined) {
|
||||
return { ok: false, reason: "validation", message: "at least one of quantity or costPrice is required" };
|
||||
}
|
||||
if (input.quantity !== undefined && (typeof input.quantity !== "number" || input.quantity <= 0)) {
|
||||
return { ok: false, reason: "validation", message: "quantity must be > 0" };
|
||||
}
|
||||
if (input.costPrice !== undefined && (typeof input.costPrice !== "number" || input.costPrice < 0)) {
|
||||
return { ok: false, reason: "validation", message: "costPrice must be >= 0" };
|
||||
}
|
||||
|
||||
const patch: { quantity?: number; cost_price?: number } = {};
|
||||
if (input.quantity !== undefined) patch.quantity = input.quantity;
|
||||
if (input.costPrice !== undefined) patch.cost_price = input.costPrice;
|
||||
|
||||
try {
|
||||
await pb.collection(STOCK_PURCHASE_ITEMS).update(input.lineId, patch);
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
const status = (err as { status?: number }).status;
|
||||
if (status === 404) {
|
||||
return { ok: false, reason: "line_not_found", message: err instanceof Error ? err.message : "line not found" };
|
||||
}
|
||||
return { ok: false, reason: "update_failed", message: err instanceof Error ? err.message : "line update failed" };
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* listOpenDrafts
|
||||
* ========================================================================== */
|
||||
|
|
|
|||
Loading…
Reference in a new issue