mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 21:42:06 +00:00
fix(uplifts): align revise validation contract
This commit is contained in:
parent
889812f90f
commit
34ebad390f
4 changed files with 101 additions and 6 deletions
|
|
@ -37,7 +37,7 @@ export function UpliftRequestForm({
|
||||||
const [evidenceFile, setEvidenceFile] = useState<File | undefined>();
|
const [evidenceFile, setEvidenceFile] = useState<File | undefined>();
|
||||||
const [formError, setFormError] = useState("");
|
const [formError, setFormError] = useState("");
|
||||||
|
|
||||||
const requiresApproval = isApprovalRequired(requested, currentNte);
|
const showApprovalFields = mode === "revise" || isApprovalRequired(requested, currentNte);
|
||||||
|
|
||||||
const submit = (event: React.FormEvent) => {
|
const submit = (event: React.FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -45,6 +45,7 @@ export function UpliftRequestForm({
|
||||||
const validationError = validateUpliftAmount(requested, currentNte, reason, {
|
const validationError = validateUpliftAmount(requested, currentNte, reason, {
|
||||||
file: evidenceFile,
|
file: evidenceFile,
|
||||||
hasExisting: hasExistingEvidence,
|
hasExisting: hasExistingEvidence,
|
||||||
|
mode,
|
||||||
});
|
});
|
||||||
setFormError(validationError);
|
setFormError(validationError);
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
|
|
@ -92,7 +93,7 @@ export function UpliftRequestForm({
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{requiresApproval && (
|
{showApprovalFields && (
|
||||||
<>
|
<>
|
||||||
<div className="vp-row">
|
<div className="vp-row">
|
||||||
<strong>
|
<strong>
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,26 @@ export type UpliftFormValues = {
|
||||||
evidenceFile?: File;
|
evidenceFile?: File;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isApprovalRequired(requested: string, currentNte: number | undefined): boolean {
|
export type UpliftMode = "create" | "revise";
|
||||||
|
|
||||||
|
export type UpliftValidationOptions = {
|
||||||
|
file?: File;
|
||||||
|
hasExisting?: boolean;
|
||||||
|
mode?: UpliftMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isApprovalRequired(
|
||||||
|
requested: string,
|
||||||
|
currentNte: number | undefined,
|
||||||
|
mode: UpliftMode = "create",
|
||||||
|
): boolean {
|
||||||
const amount = Number.parseFloat(requested);
|
const amount = Number.parseFloat(requested);
|
||||||
if (!Number.isFinite(amount)) {
|
if (!Number.isFinite(amount)) {
|
||||||
return currentNte == null;
|
return currentNte == null;
|
||||||
}
|
}
|
||||||
|
if (mode === "revise") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return currentNte == null || amount > currentNte;
|
return currentNte == null || amount > currentNte;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,19 +31,23 @@ export function validateUpliftAmount(
|
||||||
requested: string,
|
requested: string,
|
||||||
currentNte: number | undefined,
|
currentNte: number | undefined,
|
||||||
reason: string,
|
reason: string,
|
||||||
evidence: { file?: File; hasExisting?: boolean },
|
options: UpliftValidationOptions = {},
|
||||||
): string {
|
): string {
|
||||||
|
const mode = options.mode ?? "create";
|
||||||
const amount = Number.parseFloat(requested);
|
const amount = Number.parseFloat(requested);
|
||||||
if (!Number.isFinite(amount) || amount <= 0) {
|
if (!Number.isFinite(amount) || amount <= 0) {
|
||||||
return "Enter a valid requested NTE amount.";
|
return "Enter a valid requested NTE amount.";
|
||||||
}
|
}
|
||||||
if (!isApprovalRequired(requested, currentNte)) {
|
if (mode === "revise" && currentNte != null && amount <= currentNte) {
|
||||||
|
return `The revised NTE must be greater than the current NTE ($${currentNte.toFixed(2)}).`;
|
||||||
|
}
|
||||||
|
if (!isApprovalRequired(requested, currentNte, mode)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (!reason.trim()) {
|
if (!reason.trim()) {
|
||||||
return "Please include a reason for the uplift request.";
|
return "Please include a reason for the uplift request.";
|
||||||
}
|
}
|
||||||
if (!evidence.file && !evidence.hasExisting) {
|
if (!options.file && !options.hasExisting) {
|
||||||
return "Attach PDF, JPG, or PNG evidence — approval-required requests need evidence.";
|
return "Attach PDF, JPG, or PNG evidence — approval-required requests need evidence.";
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
|
|
|
||||||
48
src/test/app/v/uplift-request-form.test.tsx
Normal file
48
src/test/app/v/uplift-request-form.test.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { UpliftRequestForm } from "@/app/v/[token]/dispatch/_components/uplift-request-form";
|
||||||
|
|
||||||
|
function renderForm(overrides: Partial<React.ComponentProps<typeof UpliftRequestForm>> = {}) {
|
||||||
|
return render(
|
||||||
|
<UpliftRequestForm
|
||||||
|
mode="revise"
|
||||||
|
currentNte={1000}
|
||||||
|
initialRequested="1000"
|
||||||
|
initialReason=""
|
||||||
|
hasExistingEvidence={false}
|
||||||
|
submitting={false}
|
||||||
|
uploadingPercent={undefined}
|
||||||
|
errorMessage=""
|
||||||
|
onSubmit={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
{...overrides}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("UpliftRequestForm revise mode", () => {
|
||||||
|
it("shows reason and evidence fields regardless of amount", () => {
|
||||||
|
renderForm({ initialRequested: "900" });
|
||||||
|
|
||||||
|
expect(screen.getByLabelText(/reason/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText(/evidence/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks submit and does not call onSubmit when revised NTE is at or below current NTE", () => {
|
||||||
|
const onSubmit = vi.fn();
|
||||||
|
renderForm({ initialRequested: "1000", onSubmit });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /submit revision/i }));
|
||||||
|
|
||||||
|
expect(onSubmit).not.toHaveBeenCalled();
|
||||||
|
expect(screen.getByText(/must be greater than the current NTE/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports the current NTE threshold in the actionable error", () => {
|
||||||
|
renderForm({ initialRequested: "900", currentNte: 1000 });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /submit revision/i }));
|
||||||
|
|
||||||
|
expect(screen.getByText(/\$1000\.00/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -22,6 +22,33 @@ describe("vendor uplift workflow contract", () => {
|
||||||
expect(validateUpliftAmount("110", 100, "Emergency work", { hasExisting: true })).toBe("");
|
expect(validateUpliftAmount("110", 100, "Emergency work", { hasExisting: true })).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps create-mode requests at or below current NTE as NoApprovalRequired", () => {
|
||||||
|
expect(validateUpliftAmount("90", 100, "", { mode: "create" })).toBe("");
|
||||||
|
expect(validateUpliftAmount("100", 100, "", { mode: "create" })).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects revise-mode requests at or below current NTE with an actionable error", () => {
|
||||||
|
expect(
|
||||||
|
validateUpliftAmount("90", 100, "Reason", { hasExisting: true, mode: "revise" }),
|
||||||
|
).toMatch(/must be greater than the current NTE/i);
|
||||||
|
expect(
|
||||||
|
validateUpliftAmount("100", 100, "Reason", { hasExisting: true, mode: "revise" }),
|
||||||
|
).toMatch(/must be greater than the current NTE/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still requires approval details in revise mode when current NTE is unknown", () => {
|
||||||
|
expect(validateUpliftAmount("110", undefined, "", { mode: "revise" })).toMatch(/reason/i);
|
||||||
|
expect(validateUpliftAmount("110", undefined, "Emergency work", { mode: "revise" })).toMatch(
|
||||||
|
/evidence/i,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
validateUpliftAmount("110", undefined, "Emergency work", {
|
||||||
|
hasExisting: true,
|
||||||
|
mode: "revise",
|
||||||
|
}),
|
||||||
|
).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
it("uses a secure UUID request key", () => {
|
it("uses a secure UUID request key", () => {
|
||||||
expect(createUpliftRequestKey()).toMatch(
|
expect(createUpliftRequestKey()).toMatch(
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue