Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/validate 3 hours delay and arrivalDate can't be before departureDate #1552

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 61 additions & 4 deletions app/services/validation/__test__/buildStepValidator.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { z } from "zod";
import { flowIds } from "~/domains/flowIds";
import { buildStepValidator } from "~/services/validation/buildStepValidator";

describe("buildStepValidator", () => {
// eslint-disable-next-line sonarjs/pseudo-random
const randomIndex = Math.floor(Math.random() * flowIds.length);

describe("nested fields", () => {
it("should throw an error for an not existing field name", () => {
const schemas = {
Expand All @@ -10,7 +14,9 @@ describe("buildStepValidator", () => {
}),
};
const fieldNames = ["step2.field1"];
expect(() => buildStepValidator(schemas, fieldNames)).toThrow();
expect(() =>
buildStepValidator(schemas, fieldNames, flowIds[randomIndex]),
).toThrow();
});

it("should return a valid validation for a existing field", async () => {
Expand All @@ -21,7 +27,11 @@ describe("buildStepValidator", () => {
};
const fieldNames = ["step1.field1"];

const validator = buildStepValidator(schemas, fieldNames);
const validator = buildStepValidator(
schemas,
fieldNames,
flowIds[randomIndex],
);

// Expect a positive validation
expect(
Expand Down Expand Up @@ -57,7 +67,9 @@ describe("buildStepValidator", () => {
field1: z.string(),
};
const fieldNames = ["field2"];
expect(() => buildStepValidator(schemas, fieldNames)).toThrow();
expect(() =>
buildStepValidator(schemas, fieldNames, flowIds[randomIndex]),
).toThrow();
});

it("should return a valid validation for a existing field", async () => {
Expand All @@ -66,7 +78,11 @@ describe("buildStepValidator", () => {
};
const fieldNames = ["field1"];

const validator = buildStepValidator(schemas, fieldNames);
const validator = buildStepValidator(
schemas,
fieldNames,
flowIds[randomIndex],
);

// Expect a positive validation
expect(
Expand All @@ -85,4 +101,45 @@ describe("buildStepValidator", () => {
expect((await validator.validate({})).error).toBeDefined();
});
});

describe("specialFieldValidators", () => {
it("should return extended validation schema in fluggastrechte flow", async () => {
const schemas = {
tatsaechlicherAnkunftsDatum: z.string(),
tatsaechlicherAnkunftsZeit: z.string(),
direktAbflugsDatum: z.string(),
direktAbflugsZeit: z.string(),
};
const fieldNames = [
"tatsaechlicherAnkunftsZeit",
"tatsaechlicherAnkunftsDatum",
"direktAbflugsDatum",
"direktAbflugsZeit",
];

const validator = buildStepValidator(
schemas,
fieldNames,
"/fluggastrechte/formular",
);

expect(validator).toBeDefined();

expect(
await validator.validate({
tatsaechlicherAnkunftsDatum: "20.03.2024",
tatsaechlicherAnkunftsZeit: "14:00",
direktAbflugsDatum: "20.03.2024",
direktAbflugsZeit: "13:00",
}),
).toEqual(
expect.objectContaining({
error: {
fieldErrors: { tatsaechlicherAnkunftsZeit: "invalidTimeDelay" },
},
data: undefined,
}),
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { z } from "zod";
import { getArrivalTimeDelayValidator } from "../getArrivalTimeDelayValidator";

describe("getArrivalTimeDelayValidator", () => {
const baseSchema = z.object({
tatsaechlicherAnkunftsDatum: z.string(),
tatsaechlicherAnkunftsZeit: z.string(),
direktAbflugsDatum: z.string(),
direktAbflugsZeit: z.string(),
});

const validator = getArrivalTimeDelayValidator(baseSchema);

it("should pass validation when arrival is at least 3 hours after departure", () => {
const result = validator.safeParse({
tatsaechlicherAnkunftsDatum: "01.01.2024",
tatsaechlicherAnkunftsZeit: "14:00",
direktAbflugsDatum: "01.01.2024",
direktAbflugsZeit: "11:00",
});

expect(result.success).toBe(true);
});

it("should fail validation when arrival is less than 3 hours after departure", () => {
const result = validator.safeParse({
tatsaechlicherAnkunftsDatum: "01.01.2024",
tatsaechlicherAnkunftsZeit: "12:00",
direktAbflugsDatum: "01.01.2024",
direktAbflugsZeit: "11:00",
});

expect(result.success).toBe(false);
});

it("should handle dates across different days when arrival is at least 3 hours after departure", () => {
const result = validator.safeParse({
tatsaechlicherAnkunftsDatum: "02.01.2024",
tatsaechlicherAnkunftsZeit: "00:30",
direktAbflugsDatum: "01.01.2024",
direktAbflugsZeit: "21:00",
});

expect(result.success).toBe(true);
});

it("should handle dates across different days when arrival is less than 3 hours after departure", () => {
const result = validator.safeParse({
tatsaechlicherAnkunftsDatum: "02.01.2024",
tatsaechlicherAnkunftsZeit: "00:30",
direktAbflugsDatum: "01.01.2024",
direktAbflugsZeit: "23:00",
});

expect(result.success).toBe(false);
});

it("should handle if date is before departure date", () => {
const result = validator.safeParse({
tatsaechlicherAnkunftsDatum: "01.01.2024",
tatsaechlicherAnkunftsZeit: "00:30",
direktAbflugsDatum: "02.01.2024",
direktAbflugsZeit: "21:00",
});

expect(result.success).toBe(false);
});
});
47 changes: 43 additions & 4 deletions app/services/validation/buildStepValidator.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { withZod } from "@remix-validated-form/with-zod";
import { z } from "zod";
import { getContext } from "~/domains/contexts";
import type { FlowId } from "~/domains/flowIds";
import { parsePathname } from "~/domains/flowIds";
import { isKeyOfObject } from "~/util/objects";
import { fieldIsArray, splitArrayName } from "../array";
import {
getArrivalDateValidator,
getArrivalTimeDelayValidator,
} from "./getArrivalTimeDelayValidator";

type Schemas = Record<string, z.ZodTypeAny>;

export function buildStepValidator(schemas: Schemas, fieldNames: string[]) {
export function buildStepValidator(
schemas: Schemas,
fieldNames: string[],
flowId: FlowId,
) {
const fieldValidators: Record<string, z.ZodTypeAny> = {};

for (const fieldname of fieldNames) {
Expand All @@ -27,10 +36,40 @@ export function buildStepValidator(schemas: Schemas, fieldNames: string[]) {
fieldValidators[stepOrFieldName] = schemas[stepOrFieldName];
}
}
return withZod(z.object(fieldValidators));

return getValidatorsWithSpecialFieldValidatorsOrDefault(
fieldValidators,
fieldNames,
flowId,
);
}

export function validatorForFieldnames(fieldNames: string[], pathname: string) {
const context = getContext(parsePathname(pathname).flowId);
return buildStepValidator(context, fieldNames);
const flowId = parsePathname(pathname).flowId;
const context = getContext(flowId);
return buildStepValidator(context, fieldNames, flowId);
}

function getValidatorsWithSpecialFieldValidatorsOrDefault(
fieldValidators: Record<string, z.ZodTypeAny>,
fieldNames: string[],
flowId: FlowId,
) {
const baseSchema = z.object(fieldValidators);

if (flowId !== "/fluggastrechte/formular") return withZod(baseSchema);

const hasArrivalDate = fieldNames.includes("tatsaechlicherAnkunftsDatum");
const hasArrivalTime = fieldNames.includes("tatsaechlicherAnkunftsZeit");

const hasRequiredFields = hasArrivalDate && hasArrivalTime;

if (!hasRequiredFields) return withZod(baseSchema);

const mergedSchema = z.intersection(
getArrivalTimeDelayValidator(baseSchema),
getArrivalDateValidator(baseSchema),
);

return withZod(mergedSchema);
}
55 changes: 55 additions & 0 deletions app/services/validation/getArrivalTimeDelayValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { z } from "zod";

// Helper function to convert German date/time format to timestamp
function convertToTimestamp(date: string, time: string): number {
const [day, month, year] = date.split(".").map(Number);
const [hours, minutes] = time.split(":").map(Number);
return new Date(year, month - 1, day, hours, minutes).getTime();
}
function convertToDate(date: string): number {
const [day, month, year] = date.split(".").map(Number);
return new Date(year, month - 1, day).getTime();
}

export function getArrivalTimeDelayValidator(
baseSchema: z.ZodObject<Record<string, z.ZodTypeAny>>,
) {
return baseSchema.refine(
(data) => {
const arrivalTimestamp = convertToTimestamp(
data.tatsaechlicherAnkunftsDatum,
data.tatsaechlicherAnkunftsZeit,
);
const departureTimestamp = convertToTimestamp(
data.direktAbflugsDatum,
data.direktAbflugsZeit,
);

const minimumTimeDifferenceInMs = 3 * 60 * 60 * 1000; // 3 hours in milliseconds
const actualTimeDifferenceInMs = arrivalTimestamp - departureTimestamp;
return actualTimeDifferenceInMs >= minimumTimeDifferenceInMs;
},
{
message: "invalidTimeDelay",
path: ["tatsaechlicherAnkunftsZeit"],
},
);
}

export function getArrivalDateValidator(
baseSchema: z.ZodObject<Record<string, z.ZodTypeAny>>,
) {
return baseSchema.refine(
(data) => {
const arrivalDate = convertToDate(data.tatsaechlicherAnkunftsDatum);
const departureDate = convertToDate(data.direktAbflugsDatum);

const actualTimeDifferenceInMs = arrivalDate - departureDate;
return actualTimeDifferenceInMs >= 0;
},
{
message: "invalidDate",
path: ["tatsaechlicherAnkunftsDatum"],
},
);
}
6 changes: 5 additions & 1 deletion app/services/validation/validateFormData.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export async function validateFormData(
formData: Record<string, FormDataEntryValue>,
) {
const formDataKeys = Object.keys(formData);
const validator = buildStepValidator(getContext(flowId), formDataKeys);
const validator = buildStepValidator(
getContext(flowId),
formDataKeys,
flowId,
);
return validator.validate(formData);
}
Loading