mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 05:43:12 +00:00
378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
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<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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<number, string>([
|
|
[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." },
|
|
},
|
|
]);
|
|
});
|