diff --git a/src/app/api/plan/trigger/route.ts b/src/app/api/plan/trigger/route.ts new file mode 100644 index 0000000..dbfb34c --- /dev/null +++ b/src/app/api/plan/trigger/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +const PresignedUrlBodySchema = z.object({ + portfolio_id: z.number(), + housing_type: z.enum(["Social", "Private"]), + goal: z.enum(["Increase EPC", "Reduce energy consumption"]), + goal_value: z.string(), + trigger_file_path: z.string(), +}); + +export async function POST(request: NextRequest) { + // For the moment, this api specifically handles uploads of csvs + + const body = await request.json(); + let validatedBody; + + try { + validatedBody = PresignedUrlBodySchema.parse(body); + } catch (error) { + console.error("Invalid input: ", error); + return new NextResponse(JSON.stringify({ msg: "Invalid input" }), { + status: 400, + }); + } + + try { + // We'll trigger the plan build in our fastapi backend and then the user will just have to + // wait for the plan to be ready + + const headers = { + "x-api-key": process.env.FASTAPI_API_KEY || "", + Authorization: `Bearer ${ + request.cookies.get("next-auth.session-token")?.value + }`, + }; + + const response = await fetch( + `${process.env.FASTAPI_API_URL}/v1/plan/trigger`, + { + method: "POST", + headers: headers, + body: JSON.stringify(validatedBody), + } + ); + + if (!response.ok) { + throw new Error("API request failed"); + } + + const responseData = await response.json(); + + return new NextResponse(JSON.stringify(responseData), { + status: 200, + }); + } catch (error) { + console.error(error); + return new NextResponse(JSON.stringify({ msg: "Internal server error" }), { + status: 500, + }); + } +} diff --git a/src/app/api/upload/csv/route.ts b/src/app/api/upload/csv/route.ts index 7ff4ff4..057e420 100644 --- a/src/app/api/upload/csv/route.ts +++ b/src/app/api/upload/csv/route.ts @@ -5,14 +5,9 @@ import { z } from "zod"; const PresignedUrlBodySchema = z.object({ userId: z.number(), portfolioId: z.number(), + fileKey: z.string(), }); -function generateS3Key(userId: number, portfolioId: number, filename: string) { - const timestamp = new Date().toISOString().replace(/[:.-]/g, ""); - const key = `${userId}/${portfolioId}/${filename}-${timestamp}.csv`; - return key; -} - export async function POST(request: NextRequest) { // For the moment, this api specifically handles uploads of csvs @@ -36,12 +31,7 @@ export async function POST(request: NextRequest) { secretAccessKey: process.env.PRESIGN_AWS_SECRET_KEY, }); - const { userId, portfolioId } = validatedBody; - const fileKey = generateS3Key( - userId, - portfolioId, - "portfolio_plan_properties" - ); + const { userId, portfolioId, fileKey } = validatedBody; // Presigned url is valid for 5 minutes const preSignedUrl = await s3.getSignedUrl("putObject", { diff --git a/src/app/components/portfolio/UploadCsvModal.tsx b/src/app/components/portfolio/UploadCsvModal.tsx index 1fded6c..f6f9972 100644 --- a/src/app/components/portfolio/UploadCsvModal.tsx +++ b/src/app/components/portfolio/UploadCsvModal.tsx @@ -11,14 +11,26 @@ import { useRouter } from "next/navigation"; import { useMutation } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; +function generateS3Key(userId: number, portfolioId: number, filename: string) { + const timestamp = new Date().toISOString().replace(/[:.-]/g, ""); + const key = `${userId}/${portfolioId}/${filename}-${timestamp}.csv`; + return key; +} + async function generatePresignedUrl({ userId, portfolioId, + fileKey, }: { userId: number; portfolioId: number; + fileKey: string; }) { - const body = JSON.stringify({ userId: userId, portfolioId: portfolioId }); + const body = JSON.stringify({ + userId: userId, + portfolioId: portfolioId, + fileKey: fileKey, + }); const presignedResponse = await fetch(`/api/upload/csv`, { method: "POST", @@ -75,21 +87,33 @@ export const SubmitPlan = ({ const router = useRouter(); const session = useSession(); + const userId = session.data?.user.dbId; + + const fileKey = generateS3Key( + userId, + portfolioId, + "portfolio_plan_properties" + ); + const { mutate: mutateUploadCsv, isLoading: isUploadLoading } = useMutation( uploadCsvToS3, { onSuccess: (data) => { + const body = JSON.stringify({ + // userId: session.data?.user.dbId, + portfolio_id: portfolioId, + housing_type: housingType, + goal: goal, + goal_value: goalValue, + trigger_file_path: fileKey, + }); + // After the file has been uploaded, we can trigger the job to build the plan - // const response = fetch(`/api/plan/build`, { - // method: "POST", - // body: JSON.stringify({ - // portfolioId: portfolioId, - // housingType: housingType, - // goal: goal, - // goalValue: goalValue, - // }), - // }); - // return response; + const response = fetch(`/api/plan/trigger`, { + method: "POST", + body: body, + }); + return response; }, onError: (error) => { // handle error @@ -123,7 +147,6 @@ export const SubmitPlan = ({ router.push("/"); return null; } - const userId = session.data.user.dbId; const handlePlanBuild = () => { // The plan build is triggered by clicking submit which will: @@ -131,7 +154,7 @@ export const SubmitPlan = ({ // 2) Upload the csv to the pre-signed url // 3) Trigger the job to build the plan // 4) Redirect the user to some loading page - this could be the portfolio page itself and we just trigger a regresh with skeleton cards for the properties - mutate({ userId: userId, portfolioId: portfolioId }); + mutate({ userId: userId, portfolioId: portfolioId, fileKey: fileKey }); // TODO: Make api call to backend service to trigger the plan build // Probably need to pass in the file key to mutate (define it outside) @@ -523,7 +546,7 @@ export default function UploadCsvModal({