import { test, expect, APIRequestContext } from "@playwright/test" import { ApiSeeder, newApiContext, Warehouse, Item, Vendor } from "../support/api" import { StockTransferNewPage, StockTransferDetailPage } from "../pages/StockPages" // Stock transfer flow (Backend/ERPCore/Controllers/StockTransfersController.cs, Frontend // app/dashboard/stock/transfers/*): Draft -> Dispatch -> Receive, moving FIFO layers // between warehouses. test.describe("Stock transfers", () => { let api: APIRequestContext let seeder: ApiSeeder let srcWarehouse: Warehouse let destWarehouse: Warehouse let vendor: Vendor let item: Item test.beforeAll(async () => { api = await newApiContext() seeder = new ApiSeeder(api) srcWarehouse = await seeder.firstWarehouse() destWarehouse = await seeder.secondWarehouse() vendor = await seeder.createVendor() item = await seeder.createItem({ namePrefix: "Transfer Test Item" }) await seeder.receiveStock({ warehouseId: srcWarehouse.warehouseId, vendorId: vendor.vendorId, itemId: item.itemId, qty: 50, unitCost: 15, }) }) test.afterAll(async () => { await api.dispose() }) test("create -> dispatch -> receive moves stock between warehouses", async ({ page }) => { const before = await seeder.stockOnHand(item.itemId, srcWarehouse.warehouseId) const newPage = new StockTransferNewPage(page) await newPage.goto() await newPage.fill({ fromWarehouse: srcWarehouse.name, toWarehouse: destWarehouse.name, item: item.name, qty: 10 }) await newPage.submit() await expect(page).toHaveURL(/\/dashboard\/stock\/transfers\/\d+/) const detail = new StockTransferDetailPage(page) await detail.expectStatus("Draft") await detail.dispatch() await detail.expectStatus("InTransit") const afterDispatch = await seeder.stockOnHand(item.itemId, srcWarehouse.warehouseId) // Dispatch consumes the source FIFO layers immediately (Backend/ERPCore/Services/Stock/ // StockService.cs: "Dispatch already consumed the source layers, so this stock has left // onHand") - inTransit is reported for visibility only, not held back from onHand. expect(afterDispatch.onHand).toBeCloseTo(before.onHand - 10, 4) await detail.receive() await detail.expectStatus("Received") const destAfter = await seeder.stockOnHand(item.itemId, destWarehouse.warehouseId) expect(destAfter.onHand).toBeGreaterThanOrEqual(10) }) test("dispatch fails with insufficient stock and the transfer stays Draft", async ({ page }) => { const newPage = new StockTransferNewPage(page) await newPage.goto() await newPage.fill({ fromWarehouse: srcWarehouse.name, toWarehouse: destWarehouse.name, item: item.name, qty: 999_999, }) await newPage.submit() await expect(page).toHaveURL(/\/dashboard\/stock\/transfers\/\d+/) const detail = new StockTransferDetailPage(page) await detail.expectStatus("Draft") await detail.dispatch() // FifoCostingService rejects with 409 STOCK_NEGATIVE_BLOCKED - the frontend surfaces the // error and leaves the transfer in Draft rather than advancing it. await detail.expectStatus("Draft") await expect(page.getByRole("button", { name: /^dispatch$/i })).toBeVisible() }) })