mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +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 [formError, setFormError] = useState("");
|
||||
|
||||
const requiresApproval = isApprovalRequired(requested, currentNte);
|
||||
const showApprovalFields = mode === "revise" || isApprovalRequired(requested, currentNte);
|
||||
|
||||
const submit = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -45,6 +45,7 @@ export function UpliftRequestForm({
|
|||
const validationError = validateUpliftAmount(requested, currentNte, reason, {
|
||||
file: evidenceFile,
|
||||
hasExisting: hasExistingEvidence,
|
||||
mode,
|
||||
});
|
||||
setFormError(validationError);
|
||||
if (validationError) {
|
||||
|
|
@ -92,7 +93,7 @@ export function UpliftRequestForm({
|
|||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
{requiresApproval && (
|
||||
{showApprovalFields && (
|
||||
<>
|
||||
<div className="vp-row">
|
||||
<strong>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,26 @@ export type UpliftFormValues = {
|
|||
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);
|
||||
if (!Number.isFinite(amount)) {
|
||||
return currentNte == null;
|
||||
}
|
||||
if (mode === "revise") {
|
||||
return true;
|
||||
}
|
||||
return currentNte == null || amount > currentNte;
|
||||
}
|
||||
|
||||
|
|
@ -16,19 +31,23 @@ export function validateUpliftAmount(
|
|||
requested: string,
|
||||
currentNte: number | undefined,
|
||||
reason: string,
|
||||
evidence: { file?: File; hasExisting?: boolean },
|
||||
options: UpliftValidationOptions = {},
|
||||
): string {
|
||||
const mode = options.mode ?? "create";
|
||||
const amount = Number.parseFloat(requested);
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
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 "";
|
||||
}
|
||||
if (!reason.trim()) {
|
||||
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 "";
|
||||
|
|
|
|||
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("");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue