import { type Page } from "@playwright/test"; import { test, expect } from "../fixtures/inbound-shipment-processing"; import { InboundShipmentDetailPage } from "../pages/inbound-shipment-detail"; import { InboundShipmentEditFormPage } from "../pages/inbound-shipment-edit-form"; import { InboundShipmentListPage } from "../pages/inbound-shipment-list"; import { PurchaseOrderCreateFormPage } from "../pages/purchase-order-create-form"; import { PurchaseOrderDetailPage } from "../pages/purchase-order-detail"; import { PurchaseOrderListPage } from "../pages/purchase-order-list"; /** * E2E coverage for the `inbound-shipment-processing` business flow, driven end * to end through the live frontend as the Purchaser actor. * * An inbound shipment can only be raised against a confirmed (ORDERED) PO whose * approval seeds the supply plan, so each test first builds that precondition * through the UI: create PO -> submit -> approve. * * Clicking "Create Inbound Shipment" on the PO detail page triggers the * `createRemainingInboundShipment` mutation which creates a DRAFT GR for the * full outstanding quantity and navigates directly to that GR's detail page. * To adjust the received quantity, the Purchaser then opens the Edit form. */ const ORDER_QTY = 10; const UNIT_PRICE = "25"; /** * Create a PO with a single line and walk it to ORDERED (DRAFT -> submit -> * approve). Returns the PO docNumber so the receive flow can deep-link to it. */ async function createConfirmedPurchaseOrder(page: Page, externalRef: string): Promise { const poCreate = new PurchaseOrderCreateFormPage(page); await poCreate.goto(); await expect(poCreate.supplierCombobox).toBeVisible(); await poCreate.selectFirstSupplier(); await poCreate.selectFirstReceivingSite(); await poCreate.fillExternalReference(externalRef); await poCreate.selectFirstLineItem(0); await poCreate.setQuantity(0, String(ORDER_QTY)); await poCreate.setUnitPrice(0, UNIT_PRICE); await poCreate.submit(); const poList = new PurchaseOrderListPage(page); await expect(poList.heading).toBeVisible(); await poList.getViewLink(externalRef).first().click(); await page.waitForURL(/\/procurement\/purchase-order\/[^/]+$/); const poDetail = new PurchaseOrderDetailPage(page); await expect(poDetail.heading).toBeVisible(); // DRAFT -> SUBMITTED -> ORDERED. Approving seeds the InventorySupplyPlan rows // that back the receivable lines on the inbound shipment. await poDetail.submitButton.click(); await expect(poDetail.approveButton).toBeVisible(); await poDetail.approveButton.click(); await expect(poDetail.createInboundShipmentButton).toBeVisible(); const url = new URL(page.url()); const docNumber = url.pathname.split("/").pop(); if (!docNumber) { throw new Error("Could not resolve PO docNumber from the detail URL"); } return docNumber; } // Flow happy path: create an inbound shipment against a confirmed PO, review / // adjust a line quantity for a partial receipt, then settle it to POSTED. test("purchaser receives against a confirmed PO: create, review a partial quantity, and settle", async ({ page, }) => { const externalRef = `E2E-IN-${Date.now()}`; const poDocNumber = await createConfirmedPurchaseOrder(page, externalRef); // Clicking "Create Inbound Shipment" on the PO detail runs the // createRemainingInboundShipment mutation, which creates a DRAFT GR for the // full outstanding quantity and navigates to that GR's detail page. const poDetail = new PurchaseOrderDetailPage(page); await poDetail.goto(poDocNumber); await expect(poDetail.createInboundShipmentButton).toBeVisible(); await poDetail.createInboundShipmentButton.click(); await page.waitForURL(/\/procurement\/inbound-shipment\/[^/]+$/); const ishDetail = new InboundShipmentDetailPage(page); await expect(ishDetail.heading).toBeVisible(); await expect(ishDetail.statusBadge).toContainText(/draft/i); await expect(ishDetail.lineItemsCardTitle).toBeVisible(); // Review — open the edit form and override the received quantity downward to // reflect a partial delivery (7 of 10 arrived), leaving the shipment DRAFT. await ishDetail.editButton.click(); await page.waitForURL(/\/procurement\/inbound-shipment\/[^/]+\/edit$/); const ishEdit = new InboundShipmentEditFormPage(page); await expect(ishEdit.heading).toBeVisible(); // Receipt Date is required by the edit form; fill it before adjusting qty. await ishEdit.fillReceiptDate(new Date().toISOString().split("T")[0]); await ishEdit.getReceivedQuantityInputByIndex(0).fill("7"); await ishEdit.submit(); // Submit lands back on the inbound-shipment detail. await page.waitForURL(/\/procurement\/inbound-shipment\/[^/]+$/); await expect(ishDetail.heading).toBeVisible(); await expect(ishDetail.statusBadge).toContainText(/draft/i); await expect(ishDetail.lineItemsCardTitle).toBeVisible(); // Settle — DRAFT -> POSTED. On-hand inventory updates and a cost layer opens. await ishDetail.postButton.click(); await expect(ishDetail.statusBadge).toContainText(/posted/i); // Terminal: editing is no longer offered, and the remaining-receipt action // appears for the still-outstanding quantity on the partially-received PO. await expect(ishDetail.editButton).toHaveCount(0); await expect(ishDetail.createRemainingButton).toBeVisible(); }); // Split delivery: after a partial receipt is POSTED, the Purchaser draws a // second inbound shipment for the remaining quantity and settles it too. test("purchaser handles a split delivery: post a partial receipt, then create and settle the remaining receipt", async ({ page, }) => { const externalRef = `E2E-SPLIT-${Date.now()}`; const poDocNumber = await createConfirmedPurchaseOrder(page, externalRef); // First receipt — partial (4 of 10). "Create Inbound Shipment" creates a DRAFT // GR for the full outstanding quantity (10) and navigates to its detail page. const poDetail = new PurchaseOrderDetailPage(page); await poDetail.goto(poDocNumber); await poDetail.createInboundShipmentButton.click(); await page.waitForURL(/\/procurement\/inbound-shipment\/[^/]+$/); const ishDetail = new InboundShipmentDetailPage(page); await expect(ishDetail.statusBadge).toContainText(/draft/i); // Edit the GR down to 4 units to create a partial receipt. await ishDetail.editButton.click(); await page.waitForURL(/\/procurement\/inbound-shipment\/[^/]+\/edit$/); const ishEdit = new InboundShipmentEditFormPage(page); await expect(ishEdit.heading).toBeVisible(); await ishEdit.fillReceiptDate(new Date().toISOString().split("T")[0]); await ishEdit.getReceivedQuantityInputByIndex(0).fill("4"); await ishEdit.submit(); await page.waitForURL(/\/procurement\/inbound-shipment\/[^/]+$/); await expect(ishDetail.statusBadge).toContainText(/draft/i); await ishDetail.postButton.click(); await expect(ishDetail.statusBadge).toContainText(/posted/i); // Create the remaining inbound shipment from the POSTED receipt — the system // computes ordered - received and seeds a fresh DRAFT for the outstanding qty. await expect(ishDetail.createRemainingButton).toBeVisible(); await ishDetail.createRemainingButton.click(); // Lands on the list; a new DRAFT shipment now sits alongside the POSTED one. // Status badges render Title Case labels ("Draft", "Posted"). const ishList = new InboundShipmentListPage(page); await expect(ishList.heading).toBeVisible(); await expect(ishList.getRow("Draft").first()).toBeVisible(); await expect(ishList.getRow("Posted").first()).toBeVisible(); // Review the remaining DRAFT and edit its receipt line, then settle it. await ishList.getViewLink("Draft").first().click(); await page.waitForURL(/\/procurement\/inbound-shipment\/[^/]+$/); await expect(ishDetail.statusBadge).toContainText(/draft/i); await ishDetail.editButton.click(); await page.waitForURL(/\/procurement\/inbound-shipment\/[^/]+\/edit$/); await expect(ishEdit.heading).toBeVisible(); await ishEdit.fillReceiptDate(new Date().toISOString().split("T")[0]); await ishEdit.submit(); await page.waitForURL(/\/procurement\/inbound-shipment\/[^/]+$/); await expect(ishDetail.statusBadge).toContainText(/draft/i); await ishDetail.postButton.click(); await expect(ishDetail.statusBadge).toContainText(/posted/i); });