import { expect, test, type Route } from "@playwright/test"; const INTERNAL_USER = { token: "uplift-approver-token", expiration: new Date(Date.now() + 60 * 60 * 1000).toISOString(), email: "approver@seahavenind.com", userRoles: "Admin,Tier2Approver", fullname: "Uplift Approver", id: "1", }; async function fulfillJson(route: Route, data: unknown, status = 200) { await route.fulfill({ status, contentType: "application/json", body: JSON.stringify({ status: status < 400 ? "Success" : "Error", data }), }); } test("vendor uploads scanned evidence and submits a dispatch-scoped uplift", async ({ page }) => { let requestBody: Record | undefined; let uploadPurpose = ""; let created = false; await page.route("**/api/vendor-portal/**", async (route) => { const request = route.request(); const path = new URL(request.url()).pathname; if (path.endsWith("/session")) { await fulfillJson(route, { id: 9, companyName: "Gateway Plumbing" }); return; } if (path.endsWith("/dispatches/7/documents") && request.method() === "POST") { uploadPurpose = request.postData() ?? ""; await fulfillJson(route, { id: 22, originalFileName: "estimate.pdf", scanStatus: "Pending", reviewStatus: "Processing", purpose: "UpliftEvidence", }); return; } if (path.endsWith("/dispatches/7/documents/22/status")) { await fulfillJson(route, { id: 22, originalFileName: "estimate.pdf", scanStatus: "Passed", reviewStatus: "Approved", purpose: "UpliftEvidence", }); return; } if (path.endsWith("/dispatches/7/uplift-request") && request.method() === "POST") { requestBody = request.postDataJSON(); created = true; await fulfillJson(route, { id: 31, status: "Pending", noApprovalRequired: false, }); return; } if (path.endsWith("/dispatches/7")) { await fulfillJson(route, { id: 7, dispatchNumber: "DSP-7", poNumber: "PO-7", status: "In Progress", nteAmount: 100, checklist: [], comments: [], signoffs: [], documents: [], upliftRequests: created ? [ { id: 31, status: "Pending", currentNTE: 100, requestedNTE: 125, vendorReason: "Replacement compressor", evidenceDocumentId: 22, evidenceFileName: "estimate.pdf", notificationStatus: "Sent", }, ] : [], }); return; } await fulfillJson(route, []); }); await page.goto("/v/portal-token/dispatch/7"); await expect(page.getByRole("heading", { name: /DSP-7|PO-7/ })).toBeVisible(); await page.getByLabel("New total NTE").fill("125"); await page.getByLabel("Reason").fill("Replacement compressor"); await page.getByLabel("Evidence (PDF, JPG, PNG, HEIC, MP4, MOV)").setInputFiles({ name: "estimate.pdf", mimeType: "application/pdf", buffer: Buffer.from("%PDF-1.4\nmock estimate"), }); await page.getByRole("button", { name: "Request uplift" }).click(); await expect(page.getByText("Pending", { exact: true })).toBeVisible(); expect(uploadPurpose).toContain('name="purpose"'); expect(uploadPurpose).toContain("UpliftEvidence"); expect(requestBody).toMatchObject({ requestedNTE: 125, reason: "Replacement compressor", evidenceDocumentId: 22, }); expect(requestBody?.requestKey).toMatch(/^[0-9a-f-]{36}$/i); }); test("vendor can withdraw a pending uplift from the dispatch", async ({ page }) => { let withdrawnPath = ""; let pending = true; await page.route("**/api/vendor-portal/**", async (route) => { const request = route.request(); const path = new URL(request.url()).pathname; if (path.endsWith("/session")) { await fulfillJson(route, { id: 9, companyName: "Gateway Plumbing" }); return; } if (path.endsWith("/dispatches/7/uplift-request/31/withdraw")) { withdrawnPath = path; pending = false; await fulfillJson(route, { id: 31, status: "Withdrawn" }); return; } if (path.endsWith("/dispatches/7")) { await fulfillJson(route, { id: 7, dispatchNumber: "DSP-7", status: "In Progress", nteAmount: 100, checklist: [], comments: [], signoffs: [], documents: [], upliftRequests: [ { id: 31, status: pending ? "Pending" : "Withdrawn", currentNTE: 100, requestedNTE: 125, vendorReason: "Parts", }, ], }); return; } await fulfillJson(route, []); }); page.on("dialog", (dialog) => void dialog.accept()); await page.goto("/v/portal-token/dispatch/7"); await page.getByRole("button", { name: "Withdraw request" }).click(); await expect(page.getByText("Withdrawn", { exact: true })).toBeVisible(); expect(withdrawnPath).toBe("/api/vendor-portal/dispatches/7/uplift-request/31/withdraw"); }); test("vendor receives the no-approval result for a request within the current NTE", async ({ page, }) => { let requestBody: Record | undefined; await page.route("**/api/vendor-portal/**", async (route) => { const request = route.request(); const path = new URL(request.url()).pathname; if (path.endsWith("/session")) { await fulfillJson(route, { id: 9, companyName: "Gateway Plumbing" }); return; } if (path.endsWith("/dispatches/7/uplift-request") && request.method() === "POST") { requestBody = request.postDataJSON(); await fulfillJson(route, { noApprovalRequired: true, status: "Approved" }); return; } if (path.endsWith("/dispatches/7")) { await fulfillJson(route, { id: 7, dispatchNumber: "DSP-7", status: "In Progress", nteAmount: 100, checklist: [], comments: [], signoffs: [], documents: [], upliftRequests: [], }); return; } await fulfillJson(route, []); }); await page.goto("/v/portal-token/dispatch/7"); await page.getByLabel("New total NTE").fill("90"); await expect(page.getByLabel("Reason")).toHaveCount(0); await page.getByRole("button", { name: "Request uplift" }).click(); await expect( page.getByText("No approval required — the existing NTE already covers this amount."), ).toBeVisible(); expect(requestBody).toMatchObject({ requestedNTE: 90, reason: "" }); expect(requestBody).not.toHaveProperty("evidenceDocumentId"); }); test("vendor revises a changes-requested uplift using its existing evidence", async ({ page }) => { let revisionBody: Record | undefined; let revised = false; await page.route("**/api/vendor-portal/**", async (route) => { const request = route.request(); const path = new URL(request.url()).pathname; if (path.endsWith("/session")) { await fulfillJson(route, { id: 9, companyName: "Gateway Plumbing" }); return; } if (path.endsWith("/dispatches/7/uplift-request/31/revise")) { revisionBody = request.postDataJSON(); revised = true; await fulfillJson(route, { id: 31, status: "Pending" }); return; } if (path.endsWith("/dispatches/7")) { await fulfillJson(route, { id: 7, dispatchNumber: "DSP-7", status: "In Progress", nteAmount: 100, checklist: [], comments: [], signoffs: [], documents: [], upliftRequests: [ { id: 31, status: revised ? "Pending" : "ChangesRequested", currentNTE: 100, requestedNTE: 125, vendorReason: "Parts", decisionNote: "Itemize the estimate", evidenceDocumentId: 22, evidenceFileName: "estimate.pdf", }, ], }); return; } await fulfillJson(route, []); }); await page.goto("/v/portal-token/dispatch/7"); await page.getByRole("button", { name: "Revise" }).click(); await page.getByLabel("New total NTE").fill("120"); await page.getByLabel("Reason").fill("Parts itemized in the attached estimate"); await page.getByRole("button", { name: "Submit revision" }).click(); await expect(page.getByText("Pending", { exact: true })).toBeVisible(); expect(revisionBody).toEqual({ requestedNTE: 120, reason: "Parts itemized in the attached estimate", evidenceDocumentId: 22, }); }); test("internal approver can approve, reject, and revoke with audited notes", async ({ page }) => { const decisions: Array<{ path: string; body: unknown }> = []; const statuses = new Map([ [41, "Pending"], [42, "Pending"], ]); await page.addInitScript( (user) => localStorage.setItem("auth", JSON.stringify(user)), INTERNAL_USER, ); await page.route("**/api/Dashboard/Stats", (route) => fulfillJson(route, {})); await page.route("**/api/User*", (route) => fulfillJson(route, [])); await page.route(/\/api\/uplifts(?:[/?]|$)/, async (route) => { const request = route.request(); const path = new URL(request.url()).pathname; const action = path.match(/\/uplifts\/(41|42)\/(approve|reject|revoke)$/); if (action && request.method() === "POST") { const id = Number(action[1]); statuses.set( id, action[2] === "approve" ? "Approved" : action[2] === "revoke" ? "Revoked" : "Rejected", ); decisions.push({ path, body: request.postDataJSON() }); await fulfillJson(route, { id, status: statuses.get(id) }); return; } if (path.endsWith("/uplifts/can-approve")) { await fulfillJson(route, { canApprove: true }); return; } const requestedStatus = new URL(request.url()).searchParams.get("status"); const items = [ { id: 41, dispatchId: 7, workOrderId: 501, dispatchNumber: "DSP-41", poNumber: "PO-41", vendorCompanyName: "Gateway Plumbing", currentNTE: 100, requestedNTE: 125, delta: 25, requiredTier: 1, status: statuses.get(41), canDecide: true, evidenceDocumentId: 22, evidenceFileName: "estimate.pdf", evidenceContentType: "application/pdf", evidenceSizeBytes: 2048, }, { id: 42, dispatchId: 8, workOrderId: 502, dispatchNumber: "DSP-42", poNumber: "PO-42", vendorCompanyName: "Metro HVAC", currentNTE: 200, requestedNTE: 260, delta: 60, requiredTier: 2, status: statuses.get(42), canDecide: true, evidenceDocumentId: 23, evidenceFileName: "quote.pdf", }, ].filter((item) => requestedStatus == null || item.status === requestedStatus); await fulfillJson(route, { total: items.length, items, }); }); await page.goto("/uplifts"); await expect(page.getByRole("heading", { name: "Uplift Approvals" })).toBeVisible(); const approvalRow = page.getByRole("row").filter({ hasText: "DSP-41" }); await approvalRow.getByRole("button", { name: "Approve $25.00" }).click(); const changesRow = page.getByRole("row").filter({ hasText: "DSP-42" }); await changesRow.getByRole("button", { name: "Reject" }).click(); await page.getByLabel("Reason for rejection").fill("Please itemize labor and parts."); await page.getByRole("button", { name: "Reject uplift" }).click(); await page.getByRole("tab", { name: "Approved" }).click(); const approvedRow = page.getByRole("row").filter({ hasText: "DSP-41" }); await approvedRow.getByRole("button", { name: "Revoke" }).click(); await page.getByLabel("Reason for revoking").fill("Approval was made in error."); await page.getByRole("button", { name: "Revoke $25.00" }).click(); await expect .poll(() => decisions) .toEqual([ { path: "/api/uplifts/41/approve", body: {} }, { path: "/api/uplifts/42/reject", body: { note: "Please itemize labor and parts." }, }, { path: "/api/uplifts/41/revoke", body: { note: "Approval was made in error." }, }, ]); });