feat: Headless router - queue recording booking response (#21805)
* Add queued booking response table * Create `RoutingFormResponseRepository` * Pass `queueFormResponse` param * Queue up form response if param is passed * Forward queued form response parma to booker * Pass `queuedFormResponse` from booker to `handleNewBooking` * Write queued routing form response * Type fixes * Clean up * Allow dry run to work which wont have any QueuedFormResponse or FormResponse * Support passing the time when the modal was actually shown to the user and consider that time as the time of form submission * fix ts error * Queue -> Response through separate endpoint that would be triggered by embed * Make queueResponseId a non-guessable uid * Change queueFormResponse query param * fix ts error * Support useQueuedResponse to record new response data * revert handleNewBooking * Remove dead code formResponse * Refactor use repository * Unify migration files * refactor: moved api endpoint to app dir Signed-off-by: Omar López <zomars@me.com> * Update formResponse.ts * Refactor use-queued-response for test * Add tests * Fix ts error and unit test. recordFormResponse cant return nullish response * fix schema * feat: Support full reuse of preloaded iframe (#21803) * feat: support updating cal video settings in API v2 (#21784) * feat: support updating cal video settings in API v2 * chore: update descriptio * feat: support create event type * test: add test for updating event type * test: add test for create event type * chore: undo openapi * chore: bump libraries * Revert "chore: bump libraries" This reverts commit bdf36d09b021fc531497a7b7ea66ab9c52b7d136. * chore: bump libraries --------- Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com> Co-authored-by: supalarry <laurisskraucis@gmail.com> * fix tests and ts * Fix tests * wip-useQueuedResponseEndpoint * Add one more test * Change queueFormResponse query param * wip * Support useQueuedResponse to record new response data * Use the update useQueuedResponse endpoint * self-review addressed * Use queuedResponse if available in slots/utils * Add documentation * Remove use-queued-response from critical-path --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com> Co-authored-by: supalarry <laurisskraucis@gmail.com> * Update schema.prisma * refactor: renamed to avoid react hooks confusion Signed-off-by: Omar López <zomars@me.com> --------- Signed-off-by: Omar López <zomars@me.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com> Co-authored-by: supalarry <laurisskraucis@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
co-authored by
Lauris Skraucis
supalarry
Udit Takkar
Hariom Balhara
Omar López
Peer Richelsen
parent
9a40e54159
commit
22f136d19b
@@ -0,0 +1,152 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import { onSubmissionOfFormResponse } from "@calcom/app-store/routing-forms/lib/formSubmissionUtils";
|
||||
import { getResponseToStore } from "@calcom/app-store/routing-forms/lib/getResponseToStore";
|
||||
import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm";
|
||||
import { RoutingFormResponseRepository } from "@calcom/lib/server/repository/formResponse";
|
||||
|
||||
import { queuedResponseHandler } from "../route";
|
||||
|
||||
vi.mock("@calcom/lib/server/repository/formResponse");
|
||||
vi.mock("@calcom/app-store/routing-forms/lib/getSerializableForm");
|
||||
vi.mock("@calcom/app-store/routing-forms/lib/getResponseToStore");
|
||||
vi.mock("@calcom/app-store/routing-forms/lib/formSubmissionUtils");
|
||||
|
||||
const mockQueuedFormResponse = {
|
||||
id: "1",
|
||||
formId: "mock-form-id",
|
||||
form: {
|
||||
id: "mock-form-id",
|
||||
name: "Test Form",
|
||||
description: "Test Form Description",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
fields: [],
|
||||
routes: [],
|
||||
userId: 1,
|
||||
user: {
|
||||
id: 1,
|
||||
email: "test@example.com",
|
||||
},
|
||||
team: null,
|
||||
teamId: null,
|
||||
position: 1,
|
||||
updatedById: null,
|
||||
settings: {},
|
||||
disabled: false,
|
||||
},
|
||||
chosenRouteId: "mock-chosen-route-id",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
response: {},
|
||||
actualResponseId: 1,
|
||||
};
|
||||
|
||||
describe("queuedResponseHandler", () => {
|
||||
it("should process a queued form response", async () => {
|
||||
vi.mocked(RoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(
|
||||
mockQueuedFormResponse
|
||||
);
|
||||
|
||||
vi.mocked(getSerializableForm).mockResolvedValue({
|
||||
id: "mock-form-id",
|
||||
name: "Test Form",
|
||||
description: "Test Form Description",
|
||||
fields: [],
|
||||
routes: [],
|
||||
userId: 1,
|
||||
teamId: null,
|
||||
position: 1,
|
||||
updatedById: null,
|
||||
disabled: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
settings: {},
|
||||
} as unknown as Awaited<ReturnType<typeof getSerializableForm>>);
|
||||
|
||||
vi.mocked(getResponseToStore).mockResolvedValue({
|
||||
id: "mock-form-id",
|
||||
name: "Test Form",
|
||||
description: "Test Form Description",
|
||||
fields: [],
|
||||
routes: [],
|
||||
user: {
|
||||
id: 1,
|
||||
email: "test@example.com",
|
||||
},
|
||||
team: null,
|
||||
} as unknown as Awaited<ReturnType<typeof getResponseToStore>>);
|
||||
|
||||
vi.mocked(onSubmissionOfFormResponse).mockResolvedValue({
|
||||
id: "mock-form-id",
|
||||
name: "Test Form",
|
||||
description: "Test Form Description",
|
||||
fields: [],
|
||||
routes: [],
|
||||
user: {
|
||||
id: 1,
|
||||
email: "test@example.com",
|
||||
},
|
||||
team: null,
|
||||
} as unknown as Awaited<ReturnType<typeof onSubmissionOfFormResponse>>);
|
||||
|
||||
vi.mocked(RoutingFormResponseRepository.recordFormResponse).mockResolvedValue({
|
||||
id: "mock-form-id",
|
||||
name: "Test Form",
|
||||
description: "Test Form Description",
|
||||
fields: [],
|
||||
routes: [],
|
||||
user: {
|
||||
id: 1,
|
||||
email: "test@example.com",
|
||||
},
|
||||
team: null,
|
||||
chosenRouteId: "mock-chosen-route-id",
|
||||
} as unknown as Awaited<ReturnType<typeof RoutingFormResponseRepository.recordFormResponse>>);
|
||||
|
||||
const response = await queuedResponseHandler({
|
||||
queuedFormResponseId: "1",
|
||||
params: {},
|
||||
});
|
||||
expect(response).toEqual({
|
||||
formResponseId: "mock-form-id",
|
||||
message: "Processed",
|
||||
});
|
||||
});
|
||||
|
||||
it("if no queued form response is found, should return early", async () => {
|
||||
vi.mocked(RoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(null);
|
||||
const response = await queuedResponseHandler({
|
||||
queuedFormResponseId: "1",
|
||||
params: {},
|
||||
});
|
||||
expect(response).toEqual({
|
||||
formResponseId: null,
|
||||
message: "Already processed",
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw if form has no fields", async () => {
|
||||
vi.mocked(RoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(
|
||||
mockQueuedFormResponse
|
||||
);
|
||||
vi.mocked(getSerializableForm).mockResolvedValue({
|
||||
id: "mock-form-id",
|
||||
name: "Test Form",
|
||||
description: "Test Form Description",
|
||||
fields: undefined,
|
||||
routes: [],
|
||||
user: {
|
||||
id: 1,
|
||||
email: "test@example.com",
|
||||
},
|
||||
team: null,
|
||||
} as unknown as Awaited<ReturnType<typeof getSerializableForm>>);
|
||||
await expect(
|
||||
queuedResponseHandler({
|
||||
queuedFormResponseId: "1",
|
||||
params: {},
|
||||
})
|
||||
).rejects.toThrow("Form has no fields");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { z, ZodError } from "zod";
|
||||
|
||||
import { onSubmissionOfFormResponse } from "@calcom/app-store/routing-forms/lib/formSubmissionUtils";
|
||||
import { getResponseToStore } from "@calcom/app-store/routing-forms/lib/getResponseToStore";
|
||||
import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { RoutingFormResponseRepository } from "@calcom/lib/server/repository/formResponse";
|
||||
|
||||
import { defaultResponderForAppDir } from "../../defaultResponderForAppDir";
|
||||
|
||||
const queuedResponseSchema = z.object({
|
||||
queuedFormResponseId: z.string(),
|
||||
params: z.record(z.string(), z.string().or(z.array(z.string()))),
|
||||
});
|
||||
|
||||
export const queuedResponseHandler = async ({
|
||||
queuedFormResponseId,
|
||||
params,
|
||||
}: {
|
||||
queuedFormResponseId: string;
|
||||
params: Record<string, string | string[]>;
|
||||
}) => {
|
||||
// Get the queued response
|
||||
const queuedFormResponse = await RoutingFormResponseRepository.getQueuedFormResponseFromId(
|
||||
queuedFormResponseId
|
||||
);
|
||||
|
||||
if (!queuedFormResponse) {
|
||||
return {
|
||||
formResponseId: null,
|
||||
message: "Already processed",
|
||||
};
|
||||
}
|
||||
|
||||
const serializableForm = await getSerializableForm({
|
||||
form: queuedFormResponse.form,
|
||||
});
|
||||
|
||||
if (!serializableForm.fields) {
|
||||
throw new Error("Form has no fields");
|
||||
}
|
||||
|
||||
const response = getResponseToStore({
|
||||
formFields: serializableForm.fields,
|
||||
fieldsResponses: params,
|
||||
});
|
||||
|
||||
const formResponse = await RoutingFormResponseRepository.recordFormResponse({
|
||||
formId: queuedFormResponse.formId,
|
||||
queuedFormResponseId: queuedFormResponse.id,
|
||||
// We record new response here as that might be different from the queued response depending on if the user changed something in b/w before clicking CTA and that something wasn't prerendered
|
||||
response,
|
||||
// We use the queuedFormResponse's chosenRouteId because that is what decided routed team members
|
||||
chosenRouteId: queuedFormResponse.chosenRouteId,
|
||||
});
|
||||
|
||||
const chosenRoute = serializableForm.routes?.find((r) => r.id === queuedFormResponse.chosenRouteId);
|
||||
await onSubmissionOfFormResponse({
|
||||
form: {
|
||||
...queuedFormResponse.form,
|
||||
...serializableForm,
|
||||
},
|
||||
formResponseInDb: formResponse,
|
||||
chosenRouteAction: chosenRoute ? ("action" in chosenRoute ? chosenRoute.action : null) : null,
|
||||
});
|
||||
|
||||
return {
|
||||
formResponseId: formResponse.id,
|
||||
message: "Processed",
|
||||
};
|
||||
};
|
||||
|
||||
export const handler = async (req: NextRequest) => {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { params, queuedFormResponseId } = queuedResponseSchema.parse(body);
|
||||
const result = await queuedResponseHandler({
|
||||
queuedFormResponseId,
|
||||
params,
|
||||
});
|
||||
|
||||
return NextResponse.json({ status: "success", data: result });
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
logger.error("Invalid input", safeStringify(error));
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: "error",
|
||||
message: "Invalid input",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
logger.error("Error in queuedResponseHandler", safeStringify(error));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: "error",
|
||||
message: "Internal server error",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
Reference in New Issue
Block a user